<template>
  <div class="topology-container" @contextmenu.prevent>
    <!-- 顶部标题栏 -->
    <div class="header-section">
      <div class="title-container">
        <h1 class="main-title">RIP协议仿真系统</h1>
        <p class="subtitle">可视化网络节点管理与连接分析</p>
      </div>
    </div>

    <!-- 主内容区域 -->
    <div class="main-content">
      <!-- 左侧工具栏和状态面板 -->
      <div class="left-sidebar">
        <!-- 模式选择卡片 -->
        <div class="mode-card">
          <h3 class="card-title">工作模式</h3>
          <div class="mode-buttons">
            <el-button 
              :type="currentMode === 'select' ? 'primary' : 'default'" 
              @click="setMode('select')"
              class="mode-btn"
            >
              <el-icon><Mouse /></el-icon>
              选择模式
            </el-button>

            <el-button 
              :type="currentMode === 'add' ? 'primary' : 'default'" 
              @click="setMode('add')"
              class="mode-btn"
            >
              <el-icon><Plus /></el-icon>
              添加节点
            </el-button>

            <el-button 
              :type="currentMode === 'connect' ? 'primary' : 'default'" 
              @click="setMode('connect')"
              class="mode-btn"
            >
              <el-icon><Link /></el-icon>
              连接节点
            </el-button>
          
            <!-- 仿真控制按钮 -->
            <el-button
              :type="simRunning ? 'success' : 'primary'"
              size="small"
              class="mode-btn sim-btn"
              @click="startSimulation"
              :disabled="simRunning || nodeCount === 0"
              :title="nodeCount === 0 ? '当前拓扑没有节点，无法启动仿真' : ''"
            >
              <el-icon><Clock /></el-icon>
              开始仿真
            </el-button>

            <el-button
              size="small"
              class="mode-btn sim-btn"
              @click="openSimConfigDialog"
              :disabled="!currentSimulationId || !simRunning"
              :title="!simRunning ? '当前仿真未在运行，无法查看配置' : ''"
            >
              <el-icon><Clock /></el-icon>
              仿真配置
            </el-button>

            <el-button
              type="danger"
              size="small"
              class="mode-btn sim-btn"
              @click="stopSimulation"
              :disabled="!simRunning || !currentSimulationId || isStopping"
              :loading="isStopping"
              :title="!simRunning ? '当前未在运行' : (!currentSimulationId ? '无活动仿真 ID，无法停止' : '')"
            >
              <el-icon><Close /></el-icon>
              结束仿真
            </el-button>

            <el-button
              size="small"
              class="mode-btn sim-btn"
              @click="openSimulationRecords"
            >
              <el-icon><Clock /></el-icon>
              仿真记录
            </el-button>
            
          </div>
        </div>
        <!-- 统计信息卡片 -->
        <div class="stats-card">
          <h3 class="card-title">拓扑统计</h3>
          <div class="stats-grid">
            <div class="stat-item">
              <div class="stat-value">{{ nodeCount }}</div>
              <div class="stat-label">节点数量</div>
            </div>
            <div class="stat-item">
              <div class="stat-value">{{ edgeCount }}</div>
              <div class="stat-label">连接数量</div>
            </div>
            <div class="stat-item">
              <div class="stat-value">{{ selectedCount }}</div>
              <div class="stat-label">选中项</div>
            </div>
          </div>
        </div>

        <!-- 快速操作卡片 -->
        <div class="quick-actions-card">
          <h3 class="card-title">快速操作</h3>
          <div class="action-buttons">
            <el-button size="small" @click="clearAll" class="action-btn">
              <el-icon><Brush /></el-icon>
              清空拓扑
            </el-button>

            <el-button size="small" @click="exportJson" class="action-btn" :disabled="exportDisabled" :title="exportDisabled ? '当前无可导出的内容或未选择拓扑' : '导出拓扑'">
              <el-icon><Folder /></el-icon>
              导出拓扑
            </el-button>

            <el-button size="small" @click="importJson" class="action-btn">
              <el-icon><Upload /></el-icon>
              导入拓扑
            </el-button>

            <el-button size="small" @click="openCreateTopologyModal" class="action-btn">
              <el-icon><Plus /></el-icon>
              创建拓扑
            </el-button>

            <el-button size="small" @click="openSelectTopologyModal" class="action-btn">
              <el-icon><Folder /></el-icon>
              选择拓扑
            </el-button>
            

            

             <el-button size="small" @click="showRecentActions" class="action-btn">
              <el-icon><Clock /></el-icon>
              最近操作
            </el-button>
          </div>
        </div>
      </div>

      <!-- 中央拓扑区域 -->
      <div class="center-content">
      
        <!-- 拓扑画布 -->
        <div class="cytoscape-wrapper">
            <div ref="cyRef" class="cytoscape-container"></div>
            <!-- 放大缩小按钮组 - 画布内部右下角 -->
              <div class="zoom-controls-bottom">
                <el-button-group vertical>
                  <el-button size="small" @click="zoomIn" class="zoom-btn">
                   <el-icon><ZoomIn /></el-icon>
                  </el-button>
                  <el-button size="small" @click="zoomOut" class="zoom-btn">
                    <el-icon><ZoomOut /></el-icon>
                  </el-button>
                  <el-button size="small" @click="fitToScreen" class="zoom-btn">
                    <el-icon><FullScreen /></el-icon>
                  </el-button>
                  <el-button size="small" @click="centerContent" class="zoom-btn">
                    <el-icon><Position /></el-icon>
                  </el-button>
                </el-button-group>
               <div class="zoom-level-bottom">
                  缩放: {{ (currentZoom * 100).toFixed(0) }}%
                </div>
              </div>
            <div class="canvas-overlay">
              <div style="display:flex;align-items:center;gap:10px;">
                <div class="mode-indicator" :class="currentMode">
                  当前拓扑: {{ currentTopologyName }}
                </div>
                
                <el-button size="small" class="save-topo-btn" @click="saveActiveTopology">
                  保存
                </el-button>
              </div>
            </div>
        </div>
        <!-- 操作说明卡片 - 画布外部下方 -->
        <div class="info-card-bottom">
        <h4>操作说明</h4>
        <ul>
          <li>「添加节点」模式：点击画布空白处添加节点</li>
          <li>「连接节点」模式：依次点击两个节点建立有向边</li>
         <li>拖拽节点可以移动位置</li>
          <li>右键点击画布或节点查看更多操作</li>
        </ul>
      </div>
      </div>

      <!-- 右侧属性面板 -->
      <div class="right-sidebar">
        <!-- 最近操作面板 -->
  <div class="recent-actions-panel" v-if="showRecentActionsPanel" ref="recentPanelRef">
          <div class="panel-header">
            <h3 class="card-title">最近操作记录</h3>
            <el-button size="small" @click="showRecentActionsPanel = false" class="close-btn">
              <el-icon><Close /></el-icon>
            </el-button>
          </div>
          <div class="action-list-container">
            <div 
              v-for="(action, index) in recentActions" 
              :key="index"
              class="action-item detailed"
            >
              <el-icon class="action-icon"><InfoFilled /></el-icon>
              <div class="action-content">
                <div class="action-text">{{ action.text }}</div>
                <div class="action-time">{{ formatTime(action.timestamp) }}</div>
              </div>
            </div>
            <div v-if="recentActions.length === 0" class="empty-state">
              <el-icon><Clock /></el-icon>
              <span>暂无操作记录</span>
            </div>
          </div>
          <div class="panel-footer">
            <el-button size="small" @click="clearRecentActions" class="clear-btn">
              <el-icon><Delete /></el-icon>
              清空记录
            </el-button>
          </div>
        </div>

        <!-- 原来的属性面板（当不显示最近操作时显示） -->
        <template v-else>
          <!-- 节点详情 - 常驻显示 -->
          <div class="selection-card" ref="nodeDetailsRef">
            <h3 class="card-title">节点详情</h3>
            <div v-if="selectedNode" class="node-details">
              <div class="detail-item">
                <label>节点ID:</label>
                <span class="detail-value">{{ selectedNode.data.routerId }}</span>
              </div>
              <div class="detail-item">
                <label>节点标签:</label>
                <span class="detail-value">{{ selectedNode.data.label }}</span>
              </div>
              <div class="detail-item">
                <label>指标值:</label>
                <span class="detail-value">{{ selectedNode.data.metric }}</span>
              </div>
              <div class="detail-item">
                <label>位置:</label>
                <span class="detail-value">
                  X: {{ selectedNode.position?.x?.toFixed(2) }}, 
                  Y: {{ selectedNode.position?.y?.toFixed(2) }}
                </span>
              </div>
            </div>
            <div v-else class="empty-node-state">
              <el-icon><InfoFilled /></el-icon>
              <span>未选择节点</span>
              <p>请在画布中选择一个节点查看详情</p>
            </div>
            <div v-if="selectedNode" class="node-actions">
              <el-button size="small" @click="showPropertyModal(selectedNode.data.id)">
                <el-icon><Edit /></el-icon>
                编辑属性
              </el-button>
              <el-button size="small" type="danger" @click="deleteNode(selectedNode.data.id)">
                <el-icon><Delete /></el-icon>
                删除节点
              </el-button>
            </div>
          </div>

          <!-- 最近操作摘要 -->
          <div class="recent-actions-card">
            <h3 class="card-title">最近操作</h3>
            <div class="action-list">
              <div 
                v-for="(action, index) in recentActions.slice(0, 3)" 
                :key="index"
                class="action-item"
              >
                <el-icon class="action-icon"><InfoFilled /></el-icon>
                <span class="action-text">{{ action.text }}</span>
                <span class="action-time">{{ action.time }}</span>
              </div>
              <div v-if="recentActions.length === 0" class="empty-state">
                <span>暂无操作</span>
              </div>
            </div>
            <div class="view-all-actions">
              <el-button size="small" @click="showRecentActions" class="view-all-btn">
                查看全部
              </el-button>
            </div>
          </div>

          
        </template>
      </div>
    </div>

    <!-- 底部状态栏 -->
    <div class="footer-status">
      <div class="status-left">
        <el-icon><InfoFilled /></el-icon>
        <span>就绪 - 节点: {{ nodeCount }} | 连接: {{ edgeCount }} | 选中: {{ selectedCount }}</span>
      </div>
      <div class="status-right">
        <span>最后更新: {{ lastUpdateTime }}</span>
      </div>
    </div>

    <!-- 右键菜单 -->
    <div
      v-if="contextMenu.visible"
      class="context-menu"
      :style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }"
    >
      <template v-if="contextMenu.targetType === 'canvas'">
        <div @click="addNodeAtContext"><el-icon><Plus /></el-icon> 添加节点</div>
        <div @click="setMode('select')"><el-icon><Mouse /></el-icon> 查看属性</div>
        <div @click="setMode('connect')"><el-icon><Link /></el-icon> 连接模式</div>
        <div @click="viewSnapshot"><el-icon><Camera /></el-icon> 查看快照</div>
        <div @click="clearAll"><el-icon><Brush /></el-icon> 清空拓扑</div>
        <div @click="importJson"><el-icon><Upload /></el-icon> 导入 JSON</div>
      </template>

      <template v-else-if="contextMenu.targetType === 'node'">
        <div @click="deleteNode(contextMenu.targetId)"><el-icon><Close /></el-icon> 删除节点</div>
        <div @click="setAsSource(contextMenu.targetId)"><el-icon><Link /></el-icon> 连接</div>
        <div @click="showPropertyModal(contextMenu.targetId)"><el-icon><Mouse /></el-icon> 查看属性</div>
        <div @click="viewSnapshot(contextMenu.targetId)"><el-icon><Camera /></el-icon> 查看快照</div>
      </template>
    </div>

    <!-- 属性弹窗 -->
    <div v-if="propertyModal.visible" class="modal-overlay" @click="closePropertyModal">
      <div class="property-modal" @click.stop>
        <div class="modal-header">
          <h3>节点属性</h3>
          <button class="close-btn" @click="closePropertyModal">×</button>
        </div>
        
        <div class="modal-content">
          <div class="form-group">
            <label>路由器 ID (routerId):</label>
            <input
              type="text"
              v-model="currentNodeData.data.routerId"
              readonly
              class="readonly-input"
            />
          </div>
          
          <div class="form-group">
            <label>节点标签:</label>
            <input 
              type="text" 
              v-model="currentNodeData.data.label" 
              @input="handleLabelChange"
              placeholder="请输入节点标签"
            />
          </div>
          
          <div class="form-group">
            <label>指标值:</label>
            <input 
              type="number" 
              v-model="currentNodeData.data.metric" 
              @input="handleMetricChange"
              placeholder="请输入指标值"
            />
          </div>
          
          <div class="form-group">
            <label>位置信息:</label>
            <div class="position-info">
              X: {{ currentNodeData.position?.x?.toFixed(2) || 0 }}, 
              Y: {{ currentNodeData.position?.y?.toFixed(2) || 0 }}
            </div>
          </div>
        </div>
        
        <div class="modal-footer">
          <button class="btn btn-secondary" @click="closePropertyModal">取消</button>
          <button class="btn btn-primary" @click="saveProperties">保存</button>
        </div>
      </div>
    </div>

      <!-- 路由表对话框：在存在仿真 ID 的情况下，可查看指定路由器的路由表 -->
      <el-dialog v-model="routingTableDialogVisible" title="路由表" width="720px">
        <div style="min-height:120px;">
          <div v-if="routingTableLoading" style="text-align:center;padding:24px">加载中...</div>
          <div v-else>
            <div v-if="!routingTableData || !routingTableData.entries || routingTableData.entries.length === 0" style="padding:18px;text-align:center">
              <el-empty description="无路由表条目" />
            </div>

            <div v-else>
              <div style="margin-bottom:8px;color:var(--muted)">路由器 ID: <strong>{{ routingTableData.routerId }}</strong></div>
              <el-table :data="routingTableData.entries" style="width:100%">
                <el-table-column prop="destination" label="目标" />
                <el-table-column prop="metric" label="度量" width="100" />
                <el-table-column prop="nextHop" label="下一跳" />
                <el-table-column label="最后更新时间" :formatter="formatRoutingColumn" />
              </el-table>
            </div>
          </div>
        </div>
        <template #footer>
          <el-button @click="routingTableDialogVisible = false">关闭</el-button>
        </template>
      </el-dialog>

      <!-- 仿真配置对话框：查看当前仿真配置 -->
      <el-dialog v-model="simConfigDialogVisible" title="仿真配置" width="640px">
        <div style="min-height:120px;">
          <div v-if="simConfigLoading" style="text-align:center;padding:24px">加载中...</div>
          <div v-else>
            <div v-if="simConfigData">
              <template v-if="!simConfigEditable">
                <el-descriptions title="仿真参数" :column="1" border>
                  <el-descriptions-item label="更新间隔">
                    <div>{{ simConfigData.updateIntervalSeconds ?? simConfigData.update_interval_seconds ?? '-' }}</div>
                  </el-descriptions-item>

                  <el-descriptions-item label="路由超时">
                    <div>{{ simConfigData.routeTimeoutSeconds ?? simConfigData.route_timeout_seconds ?? '-' }}</div>
                  </el-descriptions-item>

                  <el-descriptions-item label="抑制时间">
                    <div>{{ simConfigData.holdDownSeconds ?? simConfigData.hold_down_seconds ?? '-' }}</div>
                  </el-descriptions-item>

                  <el-descriptions-item label="最大度量">
                    <div>{{ simConfigData.maxMetric ?? simConfigData.max_metric ?? '-' }}</div>
                  </el-descriptions-item>

                  <el-descriptions-item label="水平分割模式">
                    <div>{{ simConfigData.splitHorizonMode ?? simConfigData.split_horizon_mode ?? '-' }}</div>
                  </el-descriptions-item>

                  <el-descriptions-item label="触发更新">
                    <div>{{ (simConfigData.enableTriggeredUpdates ?? simConfigData.enable_triggered_updates) === undefined ? '-' : String(simConfigData.enableTriggeredUpdates ?? simConfigData.enable_triggered_updates) }}</div>
                  </el-descriptions-item>
                </el-descriptions>
              </template>

              <template v-else>
                <el-form :model="simConfigForm" ref="simConfigFormRef" label-width="120px">
                  <el-form-item label="更新间隔" prop="updateIntervalSeconds">
                    <el-input-number v-model="simConfigForm.updateIntervalSeconds" :min="0" :controls="true" />
                  </el-form-item>

                  <el-form-item label="路由超时" prop="routeTimeoutSeconds">
                    <el-input-number v-model="simConfigForm.routeTimeoutSeconds" :min="0" :controls="true" />
                  </el-form-item>

                  <el-form-item label="抑制时间" prop="holdDownSeconds">
                    <el-input-number v-model="simConfigForm.holdDownSeconds" :min="0" :controls="true" />
                  </el-form-item>

                  <el-form-item label="最大度量" prop="maxMetric">
                    <el-input-number v-model="simConfigForm.maxMetric" :min="1" :controls="true" />
                  </el-form-item>

                  <el-form-item label="水平分割模式" prop="splitHorizonMode">
                    <el-select v-model="simConfigForm.splitHorizonMode" placeholder="请选择">
                      <el-option label="NONE" value="NONE" />
                      <el-option label="SPLIT_HORIZON" value="SPLIT_HORIZON" />
                      <el-option label="POISON_REVERSE" value="POISON_REVERSE" />
                    </el-select>
                  </el-form-item>

                  <el-form-item label="触发更新" prop="enableTriggeredUpdates">
                    <el-switch v-model="simConfigForm.enableTriggeredUpdates" />
                  </el-form-item>
                </el-form>
              </template>
            </div>
            <div v-else style="padding:18px;text-align:center;color:var(--muted)">未获取到配置或该仿真尚未应用配置</div>
          </div>
        </div>
        <template #footer>
          <template v-if="!simConfigEditable">
            <el-button type="primary" @click="enterEditSimConfig" :disabled="!simConfigData">修改</el-button>
            <el-button @click="simConfigDialogVisible = false">关闭</el-button>
          </template>
          <template v-else>
            <el-button @click="cancelEditSimConfig">取消</el-button>
            <el-button type="primary" @click="saveSimConfig">保存</el-button>
          </template>
        </template>
      </el-dialog>

        <input
      ref="fileInput"
      type="file"
      accept="application/json"
      style="display: none"
      @change="handleFileChange"
    />

    <!-- 创建拓扑对话框 -->
    <el-dialog v-model="createModalVisible" title="创建拓扑" width="480px">
      <el-form :model="createFormData" :rules="createFormRules" ref="createFormRef" label-width="80px">
        <el-form-item label="名称" prop="name">
          <el-input v-model="createFormData.name" placeholder="请输入拓扑名称"></el-input>
        </el-form-item>

        <el-form-item label="描述">
          <el-input type="textarea" v-model="createFormData.description" placeholder="请输入拓扑描述" :rows="3"></el-input>
        </el-form-item>
      </el-form>

      <template #footer>
          <el-button @click="createModalVisible = false">取消</el-button>
          <el-button type="primary" @click="onConfirmCreateTopology">创建</el-button>
      </template>
    </el-dialog>
    
    <!-- 选择拓扑对话框 -->
    <el-dialog v-model="selectModalVisible" title="选择拓扑" width="720px">
      <div class="select-dialog-body" style="display:flex;flex-direction:column;gap:8px;max-height:60vh;">
        <div style="padding:8px 12px 0 12px;flex:0 0 auto;">
          <el-input v-model="searchTerm" clearable placeholder="按名称搜索拓扑" size="small" @clear="searchTerm = ''">
            <template #prefix>
              <el-icon><Clock /></el-icon>
            </template>
          </el-input>
        </div>

        <div style="flex:1 1 auto;overflow:auto;padding:12px 12px 0 12px;">
          <div v-if="loadingTopologies" style="padding: 24px; text-align:center">加载中...</div>

          <div v-else>
            <div v-if="filteredTopologyList.length === 0" class="empty-state" style="padding:24px">
              <el-icon><Clock /></el-icon>
              <span v-if="searchTerm">未找到匹配的拓扑</span>
              <span v-else>未找到任何拓扑</span>
            </div>

            <div v-else class="topo-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:12px">
              <div v-for="t in filteredTopologyList" :key="t.id" class="topo-card" style="background:var(--card-bg);padding:12px;border-radius:8px;border:1px solid rgba(0,0,0,0.04)">
                <div style="display:flex;justify-content:space-between;align-items:start;gap:8px">
                  <div style="flex:1">
                    <h4 style="margin:0 0 8px 0">{{ t.name }}</h4>
                    <div style="color:var(--muted);font-size:13px;margin-bottom:8px"><strong>详情：</strong> {{ t.description }}</div>
                    <div style="font-size:12px;color:#888">创建时间: {{ formatDate(t.createdAt) }}</div>
                  </div>
                  <div style="display:flex;flex-direction:column;gap:8px;align-items:flex-end">
                    <div style="display:flex;flex-direction:column;gap:8px;align-items:flex-end">
                      <el-button size="small" type="primary" @click="selectTopology(t)">选择</el-button>
                      <el-button size="small" @click.stop="openEditTopologyModal(t)">修改</el-button>
                      <el-button size="small" type="danger" @click.stop="deleteTopology(t.id, t.name)">删除</el-button>
                      <div v-if="String(getActiveTopologyId()) === String(t.id)" style="font-size:12px;color:green">当前激活</div>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
      <template #footer>
        <el-button @click="selectModalVisible = false">关闭</el-button>
      </template>
    </el-dialog>

    <!-- 编辑拓扑对话框 -->
    <el-dialog v-model="editModalVisible" title="修改拓扑" width="520px">
      <el-form :model="editFormData" :rules="editFormRules" ref="editFormRef" label-width="90px">
        <el-form-item label="名称" prop="name">
          <el-input v-model="editFormData.name" placeholder="请输入拓扑名称"></el-input>
        </el-form-item>

        <el-form-item label="详情">
          <el-input type="textarea" v-model="editFormData.description" :rows="4" placeholder="请输入拓扑描述"></el-input>
        </el-form-item>
      </el-form>

      <template #footer>
        <el-button @click="editModalVisible = false">取消</el-button>
        <el-button type="primary" @click="confirmEditTopology">确定</el-button>
      </template>
    </el-dialog>
    
    <!-- 仿真记录对话框 -->
  <el-dialog v-model="simRecordsDialogVisible" :title="`仿真记录 - ${currentTopologyName}`" width="760px" :close-on-click-modal="false">
        <!-- 内部滚动区域，页内滚动 -->
        <div class="sim-records-body">
          <div v-if="simInstancesLoading" style="text-align:center;padding:24px">加载中...</div>
          <div v-else>
            <div v-if="simulationInstances.length === 0" style="padding:24px;text-align:center">
              <el-empty description="未找到仿真实例" />
            </div>

            <div v-else class="sim-list" style="display:flex;flex-direction:column;gap:12px;">
              <el-card v-for="sim in simulationInstances" :key="sim.id" shadow="hover">
                <div style="display:flex;justify-content:space-between;align-items:start;gap:12px">
                  <div>
                    <h4 style="margin:0">仿真 ID: {{ sim.id }}</h4>
                    <div style="color:var(--muted);font-size:13px;margin-top:6px">状态: <strong>{{ sim.status }}</strong></div>
                    <div style="font-size:12px;color:#666;margin-top:6px">开始: {{ formatDate(sim.startedAt) }} | 结束: {{ sim.stoppedAt ? formatDate(sim.stoppedAt) : '—' }}</div>
                  </div>
                  <div style="display:flex;flex-direction:column;gap:8px;align-items:flex-end">
                    <el-button size="small" type="primary" @click="selectSimulationInstance(sim)">选择</el-button>
                    <!-- 仅当实例处于 RUNNING 状态时显示停止按钮 -->
                    <el-button
                      v-if="String(sim.status).toUpperCase() === 'RUNNING'"
                      size="small"
                      type="warning"
                      @click.stop="stopSimulationInstance(sim)"
                      :loading="stoppingMap[String(sim.id)]"
                    >
                      停止
                    </el-button>
                    <el-button size="small" type="danger" @click.stop="deleteSimulation(sim)">删除</el-button>
                  </div>
                </div>
              </el-card>
            </div>
          </div>
        </div>
        <template #footer>
          <el-button @click="simRecordsDialogVisible = false">关闭</el-button>
        </template>
      </el-dialog>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, computed, reactive, nextTick, watch } from "vue";
import cytoscape from "cytoscape";
import { Clock, Close } from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import http from '../api/http';
import StompService from '../api/stompClient.js';

const props = defineProps({
  mode: { type: String, default: "select" },
});
const emits = defineEmits(["node-selected", "topology-changed", "simulation-started", "simulation-stopped", "simulation-selected", "simulation-deleted"]);

const cyRef = ref(null);
const fileInput = ref(null);
let cy = null;
let connectSource = null;
const currentMode = ref("select");
const currentZoom = ref(1);
const selectedNode = ref(null);
// 仿真运行状态
const simRunning = ref(false);
// 是否显示仿真记录（目前仅作为占位，可扩展为对话框）
const simRecordsVisible = ref(false);
// 当前活跃的仿真 ID（后端返回），用于后续操作
function normalizeSimId(raw) {
  if (raw === null || raw === undefined) return null;
  const s = String(raw).trim();
  if (!s) return null;
  const low = s.toLowerCase();
  if (low === 'null' || low === 'undefined') return null;
  return s;
}
const currentSimulationId = ref(normalizeSimId(localStorage.getItem('currentSimulationId')));
// 停止仿真时的 loading 状态
const isStopping = ref(false);
// 每个仿真实例的停止 loading 状态（keyed by simulation id）
const stoppingMap = reactive({});

// 节点计数器 - 新增
const nodeCounter = ref(1);

// 路由表对话框状态
const routingTableDialogVisible = ref(false);
const routingTableLoading = ref(false);
const routingTableData = ref({ routerId: '', entries: [] });

// STOMP subscription state for routing table realtime updates
const stompConnected = ref(false);
const stompEndpoint = ref('/ws'); // 请根据后端实际 endpoint 修改，例如 '/ws' 或 '/stomp'
const currentRoutingSubscription = reactive({ simId: null, routerId: null });

// refs to right-side panels so we can lock their initial sizes
const recentPanelRef = ref(null);
const nodeDetailsRef = ref(null);
const lockedHeights = {
  recent: null,
  nodeDetails: null
};

// 当前激活的拓扑 ID（内存 + localStorage 持久化）
const activeTopologyId = ref(localStorage.getItem('activeTopologyId') || null);

const activeTopology = ref(null);


const currentTopologyName = computed(() => {
  try {
    if (activeTopology.value && activeTopology.value.name) return activeTopology.value.name;
    const storedName = localStorage.getItem('activeTopologyName');
    if (storedName) return storedName;
    const aid = getActiveTopologyId();
    if (aid) return `ID: ${aid}`;
  } catch (e) {
    
  }
  return '未选择';
});
function setActiveTopologyId(id) {
  // 如果切换到新的拓扑 id，则需要重置与上一拓扑相关的本地仿真上下文（但不主动停止后端仿真）
  const prevId = activeTopologyId.value;
  if (id) {
    activeTopologyId.value = String(id);
    localStorage.setItem('activeTopologyId', String(id));
    addActionRecord(`已保存激活拓扑 ID: ${id}`);

    try {
      const match = topologyList.value.find(t => String(t.id) === String(id));
      if (match) activeTopology.value = match;

      if (match && match.name) {
        try { localStorage.setItem('activeTopologyName', String(match.name)); } catch (e) { }
      }
    } catch (e) {
      // ignore
    }

    try {
      if (activeTopology.value && activeTopology.value.name) localStorage.setItem('activeTopologyName', String(activeTopology.value.name));
    } catch (e) { /* ignore */ }

    // 如果之前在另一个拓扑上有保存的仿真 id 或处于仿真运行状态，重置本地 sim 状态以便针对新拓扑能开始新的仿真。
    if (prevId && String(prevId) !== String(id)) {
      if (currentSimulationId.value || simRunning.value) {
        addActionRecord('切换拓扑：本地仿真上下文已重置');
      }
      simRunning.value = false;
      currentSimulationId.value = null;
      try { localStorage.removeItem('currentSimulationId'); } catch (e) { /* ignore */ }
    }
  } else {
    activeTopologyId.value = null;
    localStorage.removeItem('activeTopologyId');
    addActionRecord('已清除激活拓扑 ID');
    activeTopology.value = null;
    try { localStorage.removeItem('activeTopologyName'); } catch (e) { }
  }
}

function getActiveTopologyId() {
  return activeTopologyId.value;
}

// 检查指定仿真 ID 的运行状态并同步本地状态
async function fetchSimulationStatus(simId) {
  if (!simId) return null;
  try {
    // 优先请求单个仿真查询（若后端支持）
    const resp = await http.get(`/api/simulations/${simId}`);
    const data = resp?.data ?? null;
    if (data) {
      const status = (data.status ?? data.state ?? '').toString().toUpperCase();
      simRunning.value = status === 'RUNNING';
      // 保持 currentSimulationId 与本地持久化一致
      currentSimulationId.value = String(simId);
      try { localStorage.setItem('currentSimulationId', String(simId)); } catch (e) {}
      addActionRecord(`同步仿真 ${simId} 状态: ${status}`);
      return data;
    }
  } catch (err) {
    // 如果没有该接口或返回 404，回退到查询当前拓扑下的实例列表并查找
    try {
      const topoId = getActiveTopologyId();
      if (topoId) {
        const listResp = await http.get(`/api/simulations/topology/${topoId}`);
        const items = Array.isArray(listResp?.data) ? listResp.data : (listResp?.data?.items ?? []);
        const found = items.find(i => String(i.id) === String(simId) || String(i.simulationId) === String(simId) || String(i.simulation_id) === String(simId));
        if (found) {
          const status = (found.status ?? '').toString().toUpperCase();
          simRunning.value = status === 'RUNNING';
          addActionRecord(`同步仿真 ${simId} 状态: ${status}`);
          return found;
        }
      }
    } catch (err2) {
      console.warn('fetchSimulationStatus fallback failed', err2);
    }
    console.warn('fetchSimulationStatus error', err);
  }
  // 默认认为未运行
  simRunning.value = false;
  return null;
}

// 控制右侧面板显示状态
const showRecentActionsPanel = ref(false);

// 统计数据
const nodeCount = ref(0);
const edgeCount = ref(0);
const selectedCount = ref(0);

// 是否允许导出：画布必须有节点且存在激活拓扑
const exportDisabled = computed(() => {
  try {
    return (nodeCount.value === 0) || (!getActiveTopologyId());
  } catch (e) {
    return true;
  }
});

// 最近操作记录
const recentActions = ref([
  { text: '初始化拓扑画布', time: '刚刚', timestamp: Date.now() },
  { text: '系统就绪', time: '刚刚', timestamp: Date.now() }
]);

const lastUpdateTime = ref(new Date().toLocaleTimeString());

const modeLabels = {
  select: '选择模式',
  add: '添加节点',
  connect: '连接节点'
};

const contextMenu = ref({
  visible: false,
  x: 0,
  y: 0,
  targetType: null,
  targetId: null,
  cyPos: null,
});

// 属性弹窗状态
const propertyModal = ref({
  visible: false,
  nodeId: null
});

// 当前节点数据
const currentNodeData = ref({
  data: { id: '', label: '', metric: 1 },
  position: { x: 0, y: 0 }
});

// 创建拓扑模态与表单数据
const createModalVisible = ref(false);
const createFormRef = ref(null);
const createFormData = reactive({ name: '', description: '' });
const createFormRules = {
  name: [
    { required: true, message: '请输入拓扑名称', trigger: 'blur' },
    { min: 1, max: 100, message: '名称长度 1-100 个字符', trigger: 'blur' }
  ]
};

// create modal mode: 'clear' = creation via Create button (clear canvas), 'preserve' = save current canvas as topology (do not clear)
const createModalMode = ref('clear');

// 编辑拓扑对话框状态
const editModalVisible = ref(false);
const editFormRef = ref(null);
const editFormData = reactive({ id: null, name: '', description: '' });
const editFormRules = {
  name: [
    { required: true, message: '请输入拓扑名称', trigger: 'blur' },
    { min: 1, max: 100, message: '名称长度 1-100 个字符', trigger: 'blur' }
  ]
};

function openEditTopologyModal(t) {
  if (!t) return;
  editFormData.id = t.id;
  editFormData.name = t.name ?? '';
  editFormData.description = t.description ?? '';
  editModalVisible.value = true;
}

// 修改名字，详情
async function confirmEditTopology() {
  const id = editFormData.id;
  if (!id) return;

  try {
    // 简单表单校验
    if (editFormRef.value && editFormRef.value.validate) {
      try { await editFormRef.value.validate(); } catch (e) { return; }
    }

    addActionRecord(`开始修改拓扑 ${id}`);

    // 读取后端当前拓扑，获取 routers/links
    let routers = [];
    let links = [];
    try {
      const resp = await http.get(`/api/topologies/${id}`);
      const data = resp?.data || {};
      if (Array.isArray(data.routers)) routers = data.routers;
      if (Array.isArray(data.links)) links = data.links;
    } catch (e) {
      console.warn('fetch existing topology failed, will PUT with empty routers/links as fallback', e);
      routers = [];
      links = [];
    }

    const payload = {
      name: String(editFormData.name || '').trim(),
      description: String(editFormData.description || '').trim(),
      routers,
      links
    };

    
    const resp2 = await http.put(`/api/topologies/${id}`, payload);
    const updated = resp2?.data || null;

    ElMessage.success('拓扑已更新');
    addActionRecord(`已修改拓扑 ${id}`);

    try {
      const idx = topologyList.value.findIndex(item => String(item.id) === String(id));
      if (idx >= 0) {
        topologyList.value[idx] = { ...topologyList.value[idx], name: payload.name, description: payload.description };
      }
    } catch (e) { console.warn('update local list failed', e); }

   
    try {
      if (String(getActiveTopologyId()) === String(id)) {
        activeTopology.value = updated || { ...(activeTopology.value || {}), name: payload.name, description: payload.description };
        try { if (payload.name) localStorage.setItem('activeTopologyName', String(payload.name)); } catch (e) {}
      }
    } catch (e) { /* ignore */ }

    editModalVisible.value = false;
    return updated;
  } catch (err) {
    console.error('confirmEditTopology error', err);
    ElMessage.error('修改拓扑失败：' + (err?.response?.data?.message || err.message || '网络错误'));
    addActionRecord('修改拓扑失败');
    throw err;
  }
}

function openCreateTopologyModal() {
  createFormData.name = '';
  createFormData.description = '';
  createModalMode.value = 'clear';
  createModalVisible.value = true;
}

// 选择拓扑模态相关
const selectModalVisible = ref(false);
const topologyList = ref([]);
const loadingTopologies = ref(false);
const searchTerm = ref('');

const filteredTopologyList = computed(() => {
  const q = String(searchTerm.value || '').trim().toLowerCase();
  if (!q) return topologyList.value;
  return topologyList.value.filter(t => String(t.name || '').toLowerCase().includes(q));
});

function formatDate(iso) {
  try {
    return new Date(iso).toLocaleString();
  } catch (e) {
    return iso;
  }
}

async function fetchTopologies() {
  loadingTopologies.value = true;
  try {
    const resp = await http.get('/api/topologies');
    
    topologyList.value = Array.isArray(resp?.data) ? resp.data : (resp?.data?.items ?? []);
    
    try {
      const aid = getActiveTopologyId();
      if (aid) {
        const match = topologyList.value.find(t => String(t.id) === String(aid));
        if (match) activeTopology.value = match;
      }
    } catch (e) {
      
    }
  } catch (err) {
    console.error('fetchTopologies error', err);
    ElMessage.error('获取拓扑列表失败');
    topologyList.value = [];
  } finally {
    loadingTopologies.value = false;
  }
}


async function fetchActiveTopologyById() {
  const id = getActiveTopologyId();
  if (!id) {
    ElMessage.info('当前没有保存的激活拓扑 ID');
    return;
  }

  try {
    const resp = await http.get(`/api/topologies/${id}`);
    const t = resp?.data;
    if (t) {
      activeTopology.value = t;
      try { if (t.name) localStorage.setItem('activeTopologyName', String(t.name)); } catch (e) { }
      ElMessage.success('已获取激活拓扑信息');
      addActionRecord(`已从后端获取拓扑 ${id}`);
     
      try {
        renderTopology(t);
      } catch (e) {
        console.warn('renderTopology failed', e);
      }
      return t;
    } else {
      ElMessage.warning('未从后端获取到拓扑详情');
    }
  } catch (err) {
    console.error('fetchActiveTopologyById error', err);
    ElMessage.error('获取激活拓扑失败');
  }
}


function renderTopology(topo) {
  if (!cy) return;
  try {
    // 清空当前画布元素
    cy.elements().remove();

    const nodes = [];
    const edges = [];
    let maxRouterId = 0; // 新增：记录最大路由器ID

    // routers: [{ routerId, x, y, metadata }]
    if (Array.isArray(topo.routers)) {
      topo.routers.forEach((r) => {
        const rid = Number(r.routerId ?? r.router_id ?? r.id ?? '');
        if (!rid) return;
        const elId = `router_${rid}`;
        
        // 更新最大路由器ID
        maxRouterId = Math.max(maxRouterId, rid);
        
        nodes.push({
          data: {
            id: elId,
            routerId: rid,
            label: `R${rid}`, // 使用 R1, R2, R3... 格式
            metadata: r.metadata ?? null,
            metric: r.metric ?? 1,
            backendId: r.id ?? r._id ?? null,
            x: typeof r.x === 'number' ? r.x : undefined,
            y: typeof r.y === 'number' ? r.y : undefined,
          },
          position: (typeof r.x === 'number' && typeof r.y === 'number') ? { x: r.x, y: r.y } : undefined,
        });
      });
    }

    // 设置计数器为最大ID + 1 - 新增
    nodeCounter.value = maxRouterId + 1;

    // links: [{ from, to, cost }], from/to are routerIds
    if (Array.isArray(topo.links)) {
      topo.links.forEach((l, idx) => {
        const fromRid = String(l.from ?? l.src ?? '');
        const toRid = String(l.to ?? l.dst ?? '');
        if (!fromRid || !toRid) return;
        const fromEl = `router_${fromRid}`;
        const toEl = `router_${toRid}`;
        const edgeId = `edge_${fromRid}_${toRid}_${idx}`;
        edges.push({ data: { id: edgeId, source: fromEl, target: toEl, cost: l.cost ?? 1 } });
      });
    }

    // 批量添加到 cytoscape
    if (nodes.length) cy.add(nodes);
    if (edges.length) cy.add(edges);

    // 布局/调整视图
    try {
      if (nodes.some(n => n.position)) {
        cy.resize();
        cy.center();
      } else {
        cy.layout({ name: 'grid', animate: false }).run();
        cy.resize();
        cy.fit();
      }
    } catch (e) {
      console.warn('layout/render error', e);
    }

    emits('topology-changed');
    updateStatistics();
    addActionRecord(`已在画布上渲染拓扑 ${topo.id ?? topo.name ?? ''}`);
  } catch (e) {
    console.error('renderTopology error', e);
    ElMessage.error('渲染拓扑失败');
  }
}

function openSelectTopologyModal() {
  selectModalVisible.value = true;
  fetchTopologies();
}

function selectTopology(t) {
  try {
    // 保存当前拓扑对象以便右侧显示
    activeTopology.value = t;
    // 保存为激活拓扑 id（组件已有方法）
    setActiveTopologyId(t.id);
    ElMessage.success(`已选择拓扑：${t.name}`);
    selectModalVisible.value = false;
    // 立即请求并渲染该拓扑的详细数据（routers / links）
    setTimeout(() => {
      // 异步调用 fetchActiveTopologyById，忽略返回值
      fetchActiveTopologyById();
    }, 10);
  } catch (e) {
    console.error('selectTopology error', e);
    ElMessage.error('选择拓扑失败');
  }
}

// 删除指定拓扑
async function deleteTopology(id, name) {
  if (!id) return;

  function escapeHtml(str) {
    if (!str && str !== 0) return '';
    return String(str).replace(/[&<>"'`]/g, function (s) {
      return ({
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#39;',
        '`': '&#96;'
      })[s];
    });
  }

 
  try {
    await ElMessageBox.confirm(
      `<div style="line-height:1.4">
         <div style=\"font-size:14px;margin-bottom:8px\">确认要删除 <strong style=\"font-size:13px;color:#e53935\">${escapeHtml(name || id)}</strong> 吗？</div>
         <div style=\"font-size:12px;color:#666;\">此操作将永久删除该拓扑及其包含的所有路由和链路，无法恢复。</div>
       </div>`,
      '删除拓扑',
      {
        confirmButtonText: '删除',
        cancelButtonText: '取消',
        type: 'warning',
        dangerouslyUseHTMLString: true,
        distinguishCancelAndClose: true
      }
    );

    // 先查询并删除与该拓扑相关的所有仿真实例
    try {
      addActionRecord(`开始删除拓扑 ${id} 相关的仿真实例`);
      ElMessage.info('正在删除与该拓扑关联的仿真实例...');
      const simsResp = await http.get(`/api/simulations/topology/${id}`);
      const sims = Array.isArray(simsResp?.data) ? simsResp.data : (simsResp?.data?.items ?? []);
      // 按顺序删除每个仿真实例，若任一失败则中断并提示
      const failedDeletes = [];
      for (const s of sims) {
        const simId = s?.id ?? s?.simulationId ?? s?.simulation_id ?? null;
        if (!simId) continue;
        try {
          await http.delete(`/api/simulations/${simId}`);
          addActionRecord(`已删除仿真实例 ${simId}`);
        } catch (delErr) {
          console.error('delete simulation failed', simId, delErr);
          failedDeletes.push(String(simId));
          // 不立刻抛出，继续尝试删除其它实例以尽量清理
        }
      }

      if (failedDeletes.length > 0) {
        ElMessage.error(`未能删除部分仿真实例: ${failedDeletes.join(', ')}，已中止拓扑删除。请手动检查后重试。`);
        addActionRecord(`删除拓扑 ${id} 时部分仿真实例删除失败: ${failedDeletes.join(', ')}`);
        return; // 中止后续拓扑删除
      }

      // 所有相关仿真已删除，接着删除该拓扑相关的快照（如果后端提供该接口）
      try {
        addActionRecord(`开始删除拓扑 ${id} 相关的快照`);
        await http.delete(`/api/snapshots/topology/${id}`);
        addActionRecord(`已删除拓扑 ${id} 的快照`);
      } catch (snapErr) {
        console.error('delete snapshots failed', snapErr);
        ElMessage.error('删除拓扑关联快照失败：' + (snapErr?.response?.data?.message || snapErr.message || '网络错误'));
        addActionRecord(`删除拓扑 ${id} 时删除快照失败`);
        // 中止拓扑删除，避免留下部分数据（可根据需求改为忽略错误继续删除拓扑）
        return;
      }

      // 所有清理工作完成，继续删除拓扑本身
      await http.delete(`/api/topologies/${id}`);
      addActionRecord(`已删除拓扑 ${id}`);
      ElMessage.success('已删除拓扑');
    } catch (e) {
      console.error('deleteTopology orchestration error', e);
      ElMessage.error('删除拓扑失败：' + (e?.response?.data?.message || e.message || '网络错误'));
      addActionRecord(`删除拓扑失败 ${id}`);
      throw e;
    }
   
    try {
      topologyList.value = topologyList.value.filter(t => String(t.id) !== String(id));
    } catch (e) { /* ignore */ }
    // 如果删除的是当前激活的拓扑，清理激活信息
    if (String(getActiveTopologyId()) === String(id)) {
      try { setActiveTopologyId(null); } catch (e) { /* ignore */ }
      activeTopology.value = null;
      try { localStorage.removeItem('activeTopologyName'); } catch (e) {}
      // 清空画布
      try { cy && cy.elements().remove(); } catch (e) {}
      // 重置本地节点计数器，避免新节点 id 继续沿用已删除拓扑的计数
      try { nodeCounter.value = 1; } catch (e) { /* ignore */ }
    }
  } catch (err) {
    
    const msgStr = String(err || '').toLowerCase();
    const isCancel = err === 'cancel' || err === 'close' || err?.action === 'cancel' || err?.action === 'close' || msgStr.includes('cancel') || msgStr.includes('取消');
    if (isCancel) {
      
      console.log('deleteTopology cancelled by user');
      addActionRecord(`已取消删除拓扑 ${id}`);
      return;
    }

    console.error('deleteTopology error', err);
    ElMessage.error('删除拓扑失败：' + (err?.response?.data?.message || err.message || '网络错误'));
    addActionRecord('删除拓扑失败');
    throw err;
  }
}

let globalClickListener = null;
let globalResizeListener = null;

// 计算统计信息
function updateStatistics() {
  if (!cy) return;
  nodeCount.value = cy.nodes().length;
  edgeCount.value = cy.edges().length;
  selectedCount.value = cy.elements(':selected').length;
  lastUpdateTime.value = new Date().toLocaleTimeString();
}

// 添加操作记录
function addActionRecord(text) {
  const now = new Date();
  // Normalize text: remove internal prefixes like router_ or tmp_ to show cleaner business ids
  let normalized = String(text || '');
  try {
    // remove common internal prefixes
    normalized = normalized.replace(/router_/g, '');
    normalized = normalized.replace(/tmp_/g, '');
    // collapse multiple spaces
    normalized = normalized.replace(/\s+/g, ' ');
  } catch (e) {
    // fallback to original text
    normalized = String(text || '');
  }

  recentActions.value.unshift({
    text: normalized,
    time: now.toLocaleTimeString(),
    timestamp: now.getTime()
  });
  // 限制记录数量
  if (recentActions.value.length > 20) {
    recentActions.value = recentActions.value.slice(0, 20);
  }
}

// 格式化时间显示
function formatTime(timestamp) {
  const now = new Date();
  const actionTime = new Date(timestamp);
  const diff = now.getTime() - actionTime.getTime();
  
  if (diff < 60000) { // 1分钟内
    return '刚刚';
  } else if (diff < 3600000) { // 1小时内
    return `${Math.floor(diff / 60000)}分钟前`;
  } else if (diff < 86400000) { // 1天内
    return `${Math.floor(diff / 3600000)}小时前`;
  } else {
    return actionTime.toLocaleDateString();
  }
}

// 清空操作记录
function clearRecentActions() {
  recentActions.value = [];
  addActionRecord('清空操作记录');
}

function showRecentActions() {
  showRecentActionsPanel.value = !showRecentActionsPanel.value;
  if (showRecentActionsPanel.value) {
    addActionRecord('查看最近操作记录');
  }
}

onMounted(() => {
  cy = cytoscape({
    container: cyRef.value,
    elements: [],
    minZoom: 0.25,
    maxZoom: 3,
    wheelSensitivity: 1.0,
    userZoomingEnabled: true,
    userPanningEnabled: true,
    boxSelectionEnabled: false,
    style: [
      {
        selector: "node",
        style: {
          "background-color": "#0074D9",
          label: "data(label)",
          color: "#fff",
          "text-valign": "center",
          "text-halign": "center",
          "font-size": "10px",
        },
      },
      {
        selector: "edge",
        style: {
          width: 3,
          "line-color": "#aaa",
          "target-arrow-color": "#aaa",
          "target-arrow-shape": "triangle",
          "curve-style": "bezier",
        },
      },
      {
        selector: ":selected",
        style: {
          "border-width": 3,
          "border-color": "#FF4136",
        },
      },
    ],
    layout: { name: "grid" },
  });

  // 监听拓扑变化
  cy.on('add remove', () => {
    updateStatistics();
  });

  // 新增：监听选择事件 - 解决选中项统计问题
  cy.on('select', 'node', () => {
    updateStatistics();
  });

  cy.on('unselect', 'node', () => {
    updateStatistics();
  });

  cy.on('boxselect', () => {
    updateStatistics();
  });

  cy.on("tap", "node", (evt) => {
    const node = evt.target;
    try {
      if (currentMode.value === "connect") {
        handleConnectMode(node);
        return;
      }


      const nodeJson = node.json();
      
      nodeJson.data = nodeJson.data || {};
      
      nodeJson.data.id = node.id();
      
      nodeJson.data.routerId = node.data('routerId') ?? nodeJson.data.routerId ?? nodeJson.data.id;
      // metric default
      if (nodeJson.data.metric === undefined) nodeJson.data.metric = node.data('metric') ?? 1;
      // position
      const p = node.position();
      nodeJson.position = { x: p.x, y: p.y };

      selectedNode.value = nodeJson;
      emits("node-selected", nodeJson);
      // 如果存在活动仿真 ID，自动打开该路由器的路由表
      try {
        // 仅在存在有效活动仿真且仿真处于运行状态时自动打开路由表
        const hasSimId = (currentSimulationId.value !== null && currentSimulationId.value !== undefined && String(currentSimulationId.value).trim() !== '' && String(currentSimulationId.value).toLowerCase() !== 'null');
        if (hasSimId && simRunning.value) {
          openRoutingTableForNode(nodeJson);
        }
      } catch (e) { /* ignore */ }
    } catch (e) {
      console.warn('tap node handler error', e);
    }
  });

  // 实时监听节点位置变化：当被选中节点移动时，立刻更新右侧详情卡与属性弹窗的数据
  cy.on('position', 'node', (evt) => {
    try {
      const node = evt.target;
     
      if (selectedNode.value && selectedNode.value.data && String(selectedNode.value.data.id) === String(node.id())) {
        
        const pos = node.position();
        selectedNode.value.position = { x: pos.x, y: pos.y };
      }

      
      if (propertyModal.value && propertyModal.value.visible && String(propertyModal.value.nodeId) === String(node.id())) {
        const pos = node.position();
        
        currentNodeData.value.position = { x: pos.x, y: pos.y };
      }
    } catch (e) {
      console.warn('position event handler error', e);
    }
  });

  // 在节点拖拽结束时将坐标保存到后端（仅当后端已返回 router 的 backendId）
  cy.on('free', 'node', async (evt) => {
    try {
      const node = evt.target;
      const backendId = node.data('backendId');
      
      const justCreatedAt = node.data('justCreatedAt');
      if (justCreatedAt && (Date.now() - Number(justCreatedAt)) < 800) {
        
        addActionRecord(`节点 ${node.id()} 刚创建，跳过初始位置保存（短暂延迟）`);
        return;
      }

      // 如果没有后端 id，则不尝试保存（后端可能还未创建该路由）
      if (!backendId) {
        addActionRecord(`点击保存确定修改`);
        return;
      }

      const pos = node.position();
      const payload = { x: pos.x, y: pos.y };

      // 调用后端接口保存坐标（使用相对路径，http 客户端会处理 base URL）
      // 兼容性策略：优先尝试 PUT（更新语义），若被拒绝再降级到 POST；最后尝试 PATCH。
      try {
        // 首先尝试使用 PUT（大多数后端会把坐标更新视为资源更新）
        await http.put(`/api/topologies/router/${backendId}/position`, payload);
        node.data('lastPositionSavedAt', Date.now());
        node.data('lastPositionSavedOk', true);
        addActionRecord(`已保存路由器 ${backendId} 的坐标 (${pos.x.toFixed(2)}, ${pos.y.toFixed(2)})`);
      } catch (err) {
        console.error('save position API error (PUT)', err);
        const status = err?.response?.status;
        const respData = err?.response?.data;

        // 如果 PUT 被禁止或未实现，回退到 POST
        if (status === 404 || status === 405) {
          try {
            await http.post(`/api/topologies/router/${backendId}/position`, payload);
            node.data('lastPositionSavedAt', Date.now());
            node.data('lastPositionSavedOk', true);
            addActionRecord(`已保存路由器 ${backendId} 的坐标 (${pos.x.toFixed(2)}, ${pos.y.toFixed(2)})（POST 回退成功）`);
            return;
          } catch (err2) {
            console.error('save position POST fallback error', err2);
            // 再尝试 PATCH（部分后端可能只接受 PATCH 更新）
            try {
              await http.patch(`/api/topologies/router/${backendId}/position`, payload);
              node.data('lastPositionSavedAt', Date.now());
              node.data('lastPositionSavedOk', true);
              addActionRecord(`已保存路由器 ${backendId} 的坐标 (${pos.x.toFixed(2)}, ${pos.y.toFixed(2)})（PATCH 回退成功）`);
              return;
            } catch (err3) {
              console.error('save position PATCH fallback error', err3);
              node.data('lastPositionSavedOk', false);
              node.data('lastPositionSavedError', String(err3?.message || err3));
              addActionRecord(`保存路由器 ${backendId} 坐标失败（所有尝试均失败）: ${err3?.response?.data?.message || err3.message || err3}`);
              ElMessage.error('保存坐标失败：' + (err3?.response?.data?.message || err3.message || '网络错误'));
              return;
            }
          }
        }

        // 其它错误（权限、校验、网络等）直接展示后端返回信息
        node.data('lastPositionSavedOk', false);
        node.data('lastPositionSavedError', String(respData?.message || err.message || respData || err));
        addActionRecord(`保存路由器 ${backendId} 坐标失败: ${respData?.message ?? err.message ?? JSON.stringify(respData)}`);
        ElMessage.error('保存坐标失败：' + (respData?.message || err.message || '网络错误'));
      }
    } catch (e) {
      console.warn('free event handler error', e);
    }
  });

  // 监听缩放
  cy.on('zoom', (evt) => {
    currentZoom.value = cy.zoom();
  });

  const MIN_ZOOM = 0.25;
  const MAX_ZOOM = 3;
  let zoomClampScheduled = false;
  cy.on('zoom', () => {
    if (zoomClampScheduled) return;
    zoomClampScheduled = true;
    requestAnimationFrame(() => {
      try {
        const z = cy.zoom();
        if (z < MIN_ZOOM) cy.zoom(MIN_ZOOM);
        else if (z > MAX_ZOOM) cy.zoom(MAX_ZOOM);
      } catch (e) {
        console.error('zoom clamp error', e);
      }
      zoomClampScheduled = false;
    });
  });

  globalResizeListener = () => {
    try {
      cy.resize();
    } catch (e) {
      console.error('cy.resize error', e);
    }
  };
  window.addEventListener('resize', globalResizeListener);

  cy.on("tap", (evt) => {
    if (evt.target === cy && currentMode.value === "add") {
      const pos = evt.position;
      addNode(pos);
    }
  });

  cy.on("cxttap", (evt) => {
    contextMenu.value.visible = false;
    setTimeout(() => {
      contextMenu.value.visible = true;
      contextMenu.value.x = evt.originalEvent.clientX;
      contextMenu.value.y = evt.originalEvent.clientY;
      contextMenu.value.cyPos = evt.position;

      if (evt.target === cy) {
        contextMenu.value.targetType = "canvas";
        contextMenu.value.targetId = null;
      } else if (evt.target.isNode()) {
        contextMenu.value.targetType = "node";
        contextMenu.value.targetId = evt.target.id();
      }
    }, 0);
  });

  globalClickListener = (e) => {
    const menuEl = document.querySelector(".context-menu");
    if (!menuEl || !menuEl.contains(e.target)) {
      contextMenu.value.visible = false;
    }
  };
  document.addEventListener("click", globalClickListener);

  // 初始统计
  updateStatistics();

  // 新增：检查并加载激活的拓扑 - 解决重新进入页面时画布与显示不同步问题
  const activeId = getActiveTopologyId();
  if (activeId) {
    // 延迟执行，确保 cytoscape 完全初始化
    setTimeout(() => {
      fetchActiveTopologyById();
    }, 100);
  }

  // 如果本地有保存的 currentSimulationId，尝试同步其运行状态（返回页面或刷新后恢复 stop 按钮可用性）
  try {
    const localSim = currentSimulationId.value;
    if (localSim) {
      // 不阻塞渲染，异步同步状态
      fetchSimulationStatus(localSim).catch(e => console.warn('sync sim status failed', e));
    }
  } catch (e) {
    console.warn('initial sim status sync error', e);
  }

  
  nextTick(() => {
    try {
      if (recentPanelRef.value && !lockedHeights.recent) {
        const h = recentPanelRef.value.offsetHeight;
        lockedHeights.recent = h;
        recentPanelRef.value.style.height = h + 'px';
        recentPanelRef.value.style.minHeight = h + 'px';
        recentPanelRef.value.style.maxHeight = h + 'px';
        recentPanelRef.value.style.overflow = 'auto';
      }
      if (nodeDetailsRef.value && !lockedHeights.nodeDetails) {
        const h2 = nodeDetailsRef.value.offsetHeight;
        lockedHeights.nodeDetails = h2;
        nodeDetailsRef.value.style.height = h2 + 'px';
        nodeDetailsRef.value.style.minHeight = h2 + 'px';
        nodeDetailsRef.value.style.maxHeight = h2 + 'px';
        nodeDetailsRef.value.style.overflow = 'auto';
      }
    } catch (e) {
      console.warn('lock heights failed', e);
    }
  });
});


watch(showRecentActionsPanel, async (val) => {
  await nextTick();
  try {
    if (val) {
      
      if (recentPanelRef.value && !lockedHeights.recent) {
        const h = recentPanelRef.value.offsetHeight;
        lockedHeights.recent = h;
        recentPanelRef.value.style.height = h + 'px';
        recentPanelRef.value.style.minHeight = h + 'px';
        recentPanelRef.value.style.maxHeight = h + 'px';
        recentPanelRef.value.style.overflow = 'auto';
      }
    } else {
      
      if (nodeDetailsRef.value && !lockedHeights.nodeDetails) {
        const h2 = nodeDetailsRef.value.offsetHeight;
        lockedHeights.nodeDetails = h2;
        nodeDetailsRef.value.style.height = h2 + 'px';
        nodeDetailsRef.value.style.minHeight = h2 + 'px';
        nodeDetailsRef.value.style.maxHeight = h2 + 'px';
        nodeDetailsRef.value.style.overflow = 'auto';
      }
    }
  } catch (e) {
    console.warn('watch lock heights failed', e);
  }
});

onUnmounted(() => {
  if (globalClickListener) {
    document.removeEventListener("click", globalClickListener);
  }
});

function setMode(mode) {
  currentMode.value = mode;
  connectSource = null;
  cy.elements().unselect();
  contextMenu.value.visible = false;
  selectedNode.value = null;
  addActionRecord(`切换到${modeLabels[mode]}`);
}

// 仿真控制：开始仿真
async function startSimulation() {
  if (simRunning.value) return;
  // Prevent starting simulation when there are no nodes in the current topology
  try {
    if (!cy || cy.nodes().length === 0) {
      ElMessage.warning('当前拓扑中没有节点，无法启动仿真');
      addActionRecord('开始仿真失败：无节点');
      return;
    }
  } catch (e) {
    // ignore cy inspection errors
  }
  const topoId = getActiveTopologyId();
  if (!topoId) {
    ElMessage.warning('请先选择或创建激活拓扑，再开始仿真');
    addActionRecord('开始仿真失败：无激活拓扑');
    return;
  }

  
  const url = `/api/simulations/${topoId}/start`;
  const payload = { name: activeTopology.value?.name ?? undefined };

  try {
    // 向后端发起启动仿真请求（发送 JSON body），与项目中其它接口调用一致
    const resp = await http.post(url, payload);
    console.log('startSimulation response:', resp);
    const respData = resp?.data;

    simRunning.value = true;
    // 解析后端返回的仿真 id（可能字段名不同）
    // 后端有时直接返回一个数字/字符串（示例：resp.data = 3），也可能返回对象 { id: ... }
    const returnedSimId = (respData !== null && (typeof respData === 'number' || typeof respData === 'string'))
      ? respData
      : (respData?.id ?? respData?.simulationId ?? respData?.simulation_id ?? null);
    if (returnedSimId) {
      currentSimulationId.value = String(returnedSimId);
      try { localStorage.setItem('currentSimulationId', String(returnedSimId)); } catch (e) { /* ignore */ }
    }
    addActionRecord(`已开始仿真 ${topoId}` + (returnedSimId ? ` (id: ${returnedSimId})` : ''));
    ElMessage.success('仿真已开始');

    // 通知外部组件（例如 SnapshotAnalysis）刷新仿真列表
    try { emits('simulation-started', respData); } catch (e) { /* ignore if listener absent */ }

    // 立即下发空配置给后端，后端会使用其内建默认值
    try {
      if (returnedSimId) {
        await http.put(`/api/simulations/${returnedSimId}/config`, {});
        addActionRecord(`已下发空配置到仿真 ${returnedSimId}`);
        console.log('sent empty config to simulation', returnedSimId);
      }
    } catch (cfgErr) {
      console.error('apply default config failed', cfgErr);
      // 只提示不阻塞仿真启动流程
      ElMessage.error('下发仿真配置失败：' + (cfgErr?.response?.data?.message || cfgErr.message || '网络错误'));
    }

    // 返回后端响应以便调用者进一步处理（如果需要）
    return respData;
  } catch (err) {
    console.error('startSimulation error', err);
    simRunning.value = false;
    addActionRecord(`开始仿真失败 ${topoId}`);
    ElMessage.error('开始仿真失败：' + (err?.response?.data?.message || err.message || '网络错误'));
    // 返回错误对象（不抛出），调用者可根据需要处理
    return null;
  }
}
// 仿真控制：结束仿真（调用后端停用接口）
async function stopSimulation() {
  if (!simRunning.value) return;
  const simId = currentSimulationId.value;
  if (!simId) {
    ElMessage.warning('当前没有活动的仿真 ID，无法停止');
    addActionRecord('结束仿真失败：无仿真ID');
    return;
  }

  isStopping.value = true;
  addActionRecord(`请求停止仿真 ${simId}`);
  const url = `/api/simulations/stop/${simId}`;

  try {
    const resp = await http.post(url, {});
    console.log('stopSimulation response:', resp);
    const respData = resp?.data;

    simRunning.value = false;
    addActionRecord(`已停止仿真 ${simId}`);
    ElMessage.success('仿真已停止');

    // 清理本地保存的仿真 id
    currentSimulationId.value = null;
    try { localStorage.removeItem('currentSimulationId'); } catch (e) { /* ignore */ }

    // 通知外部组件（如需）
    try { emits('simulation-stopped', respData); } catch (e) { /* ignore */ }

    return respData;
  } catch (err) {
    console.error('stopSimulation error', err);
    addActionRecord(`停止仿真失败 ${simId}`);
    ElMessage.error('停止仿真失败：' + (err?.response?.data?.message || err.message || '网络错误'));
    return null;
  } finally {
    isStopping.value = false;
  }
}
// 打开仿真记录面板（当前为占位行为，后续可扩展为独立记录对话框）
function openSimulationRecords() {
  // 打开仿真记录：请求后端该拓扑的所有仿真实例并打开对话框展示
  const topoId = getActiveTopologyId();
  if (!topoId) {
    ElMessage.warning('请先选择一个拓扑后查看仿真记录');
    return;
  }

  fetchSimulationInstances(topoId);
}

// 仿真实例数据与对话框状态
const simulationInstances = ref([]);
const simRecordsDialogVisible = ref(false);
const simInstancesLoading = ref(false);

async function fetchSimulationInstances(topologyId) {
  simInstancesLoading.value = true;
  try {
    const resp = await http.get(`/api/simulations/topology/${topologyId}`);
    const items = Array.isArray(resp?.data) ? resp.data : (resp?.data?.items ?? []);
    // 按创建时间降序
    simulationInstances.value = items.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
    simRecordsDialogVisible.value = true;
    addActionRecord(`已加载拓扑 ${topologyId} 的仿真记录（${simulationInstances.value.length} 条）`);
  } catch (err) {
    console.error('fetchSimulationInstances error', err);
    ElMessage.error('获取仿真记录失败');
  } finally {
    simInstancesLoading.value = false;
  }
}

// 选择某个仿真实例：保存 id，并设置本地运行状态
function selectSimulationInstance(sim) {
  if (!sim) return;
  const simId = sim.id ?? sim.simulationId ?? sim.simulation_id ?? null;
  if (!simId) {
    ElMessage.warning('该仿真实例没有可用的 ID');
    return;
  }

  currentSimulationId.value = String(simId);
  try { localStorage.setItem('currentSimulationId', String(simId)); } catch (e) { /* ignore */ }

  // 根据返回的状态设置 simRunning
  simRunning.value = String(sim.status).toUpperCase() === 'RUNNING';
  // 额外同步后端最新状态（若列表里状态字段不准确或有延迟）
  try { fetchSimulationStatus(simId).catch(() => {}); } catch (e) {}

  addActionRecord(`已选择仿真 ${simId}`);
  ElMessage.success(`已选择仿真 ${simId}`);

  // 关闭对话框并通知外部
  simRecordsDialogVisible.value = false;
  try { emits('simulation-selected', { id: simId, raw: sim }); } catch (e) { /* ignore */ }
}

// 删除指定仿真实例
async function deleteSimulation(sim) {
  if (!sim) return;
  const simId = sim.id ?? sim.simulationId ?? sim.simulation_id ?? null;
  if (!simId) {
    ElMessage.warning('该仿真实例没有可用的 ID');
    return;
  }

  try {
    await ElMessageBox.confirm(`确认要删除仿真 ${simId} 吗？此操作不可恢复。`, '删除仿真', {
      confirmButtonText: '删除',
      cancelButtonText: '取消',
      type: 'warning'
    });
  } catch (e) {
    // user cancelled
    return;
  }

  try {
    const resp = await http.delete(`/api/simulations/${simId}`);
    console.log('deleteSimulation response:', resp);
    // 从本地列表移除
    simulationInstances.value = simulationInstances.value.filter(item => String(item.id) !== String(simId));
    addActionRecord(`已删除仿真 ${simId}`);
    ElMessage.success(`已删除仿真 ${simId}`);

    // 如果删除的是当前选中仿真，清理本地状态
    if (String(currentSimulationId.value) === String(simId)) {
      currentSimulationId.value = null;
      try { localStorage.removeItem('currentSimulationId'); } catch (e) { /* ignore */ }
      simRunning.value = false;
    }

    try { emits('simulation-deleted', { id: simId, raw: sim }); } catch (e) { /* ignore */ }
    return resp?.data ?? null;
  } catch (err) {
    console.error('deleteSimulation error', err);
    ElMessage.error('删除仿真失败：' + (err?.response?.data?.message || err.message || '网络错误'));
    addActionRecord(`删除仿真失败 ${simId}`);
    return null;
  }
}

  // 停止指定仿真实例（用于仿真记录列表的单条停止按钮）
  async function stopSimulationInstance(sim) {
    if (!sim) return;
    const simId = sim.id ?? sim.simulationId ?? sim.simulation_id ?? null;
    if (!simId) {
      ElMessage.warning('该仿真实例没有可用的 ID');
      return;
    }

    const key = String(simId);
    // 设置 per-item loading
    try { stoppingMap[key] = true; } catch (e) { /* ignore */ }
    addActionRecord(`请求停止仿真 ${key}`);

    try {
      const resp = await http.post(`/api/simulations/stop/${key}`, {});
      const respData = resp?.data;

      // 更新本地列表项状态（若后端返回新的状态则使用，否则标记为 STOPPED）
      try {
        const idx = simulationInstances.value.findIndex(s => String(s.id) === key || String(s.simulationId) === key || String(s.simulation_id) === key);
        if (idx >= 0) {
          simulationInstances.value[idx].status = (respData && respData.status) ? respData.status : 'STOPPED';
          simulationInstances.value[idx].stoppedAt = respData?.stoppedAt ?? new Date().toISOString();
        }
      } catch (e) { console.warn('update simulationInstances after stop failed', e); }

      // 如果停止的是当前选中仿真，则清理本地运行状态
      if (String(currentSimulationId.value) === key) {
        simRunning.value = false;
        currentSimulationId.value = null;
        try { localStorage.removeItem('currentSimulationId'); } catch (e) { /* ignore */ }
      }

      ElMessage.success('仿真已停止');
      addActionRecord(`已停止仿真 ${key}`);
      try { emits('simulation-stopped', { id: key, raw: respData }); } catch (e) { /* ignore */ }
      return respData;
    } catch (err) {
      console.error('stopSimulationInstance error', err);
      ElMessage.error('停止仿真失败：' + (err?.response?.data?.message || err.message || '网络错误'));
      addActionRecord(`停止仿真失败 ${key}`);
      return null;
    } finally {
      try { stoppingMap[key] = false; } catch (e) { /* ignore */ }
    }
  }

  // 格式化路由表时间，兼容 ISO 字符串或 { seconds,nanos } 形式
  function formatRoutingTime(raw) {
    try {
      if (!raw) return '';
      if (typeof raw === 'string') {
        const d = new Date(raw);
        if (isNaN(d.getTime())) return String(raw);
        return d.toLocaleString();
      }
      if (typeof raw === 'object') {
        // 支持 { seconds: number, nanos: number } 或 { seconds: '...', nanos: '...' }
        const s = Number(raw.seconds ?? raw.sec ?? raw.secondsValue ?? 0);
        const n = Number(raw.nanos ?? raw.nanoseconds ?? 0);
        const ms = (s * 1000) + Math.floor(n / 1e6);
        const d2 = new Date(ms);
        if (isNaN(d2.getTime())) return JSON.stringify(raw);
        return d2.toLocaleString();
      }
      return String(raw);
    } catch (e) {
      return String(raw);
    }
  }

  // Element table formatter wrapper
  function formatRoutingColumn(row) {
    return formatRoutingTime(row?.lastUpdated);
  }

  // 从后端获取指定仿真下指定路由器的路由表
  async function fetchRoutingTable(simId, routerId) {
    if (!simId || !routerId) {
      ElMessage.warning('缺少仿真 ID 或 路由器 ID，无法获取路由表');
      return null;
    }

    routingTableLoading.value = true;
    try {
      const resp = await http.get(`/api/simulations/${simId}/routers/${routerId}/routing-table`);
      const data = resp?.data || {};
      // 归一化 entries
      const entries = Array.isArray(data.entries) ? data.entries : (Array.isArray(data.routeEntries) ? data.routeEntries : []);
      routingTableData.value = { routerId: data.routerId ?? routerId, entries };
      routingTableDialogVisible.value = true;
      addActionRecord(`加载路由表 ${routerId}（仿真 ${simId}）`);
      return routingTableData.value;
    } catch (err) {
      console.error('fetchRoutingTable error', err);
      ElMessage.error('获取路由表失败：' + (err?.response?.data?.message || err.message || '网络错误'));
      return null;
    } finally {
      routingTableLoading.value = false;
    }
  }

// STOMP helper: ensure connected
async function ensureStompConnected() {
  if (stompConnected.value && StompService && StompService.connected) return;
  try {
    await StompService.connect(stompEndpoint.value);
    stompConnected.value = true;
  } catch (e) {
    console.error('STOMP connect failed', e);
    stompConnected.value = false;
  }
}

// Start realtime subscription for routing table updates of a specific sim/router
async function startRoutingTableSubscription(simId, routerId) {
  if (!simId || !routerId) return;
  try {
    await ensureStompConnected();
    // unsubscribe existing
    try { StompService.unsubscribe(currentRoutingSubscription.simId, currentRoutingSubscription.routerId); } catch (e) {}
    currentRoutingSubscription.simId = simId; currentRoutingSubscription.routerId = routerId;
    StompService.subscribeRoutingTable(simId, routerId, (payload) => {
      try {
        // payload is expected to be routing table JSON or entries; normalize
        const data = payload || {};
        const entries = Array.isArray(data.entries) ? data.entries : (Array.isArray(data.routeEntries) ? data.routeEntries : []);
        routingTableData.value = { routerId: data.routerId ?? routerId, entries };
        // ensure dialog open
        routingTableDialogVisible.value = true;
        addActionRecord(`收到路由表实时更新（仿真 ${simId} 路由器 ${routerId}）`);
      } catch (e) {
        console.error('Failed to handle routing table stomp message', e, payload);
      }
    });
  } catch (e) {
    console.error('startRoutingTableSubscription error', e);
  }
}

function stopRoutingTableSubscription() {
  try {
    if (currentRoutingSubscription.simId && currentRoutingSubscription.routerId) {
      StompService.unsubscribe(currentRoutingSubscription.simId, currentRoutingSubscription.routerId);
    }
  } catch (e) {
    console.warn('unsubscribe failed', e);
  } finally {
    currentRoutingSubscription.simId = null; currentRoutingSubscription.routerId = null;
  }
}

  // 根据节点对象（node.json()）打开该节点对应的路由表（使用 currentSimulationId）
  function openRoutingTableForNode(nodeJson) {
    if (!nodeJson) return;
    const routerId = nodeJson.data?.routerId ?? nodeJson.data?.id ?? nodeJson.id ?? null;
    const simId = currentSimulationId.value;
    if (!simId) {
      ElMessage.info('当前无活动仿真 ID，无法查看路由表');
      return;
    }

    // 先从后端拉取一次静态路由表并打开对话框
    fetchRoutingTable(simId, routerId).then((data) => {
      // 尝试建立 STOMP 订阅以获取实时更新
      try {
        // 默认使用当前页面同源的 /ws 路径，若后端在其它地址或路径，请先设置 stompEndpoint.value
        if (!stompEndpoint.value || stompEndpoint.value === '/ws') {
          // 设置为完整 URL，便于 sockjs 进行连接
          try { stompEndpoint.value = window.location.origin + '/ws'; } catch (e) { /* ignore */ }
        }
        startRoutingTableSubscription(simId, routerId);
      } catch (e) {
        console.warn('startRoutingTableSubscription failed', e);
      }
    }).catch((e) => {
      console.warn('fetchRoutingTable initial load failed', e);
    });
  }

// 当路由表对话框关闭时停止订阅
watch(routingTableDialogVisible, (val) => {
  if (!val) {
    stopRoutingTableSubscription();
  }
});

// 仿真配置查看逻辑
const simConfigDialogVisible = ref(false);
const simConfigLoading = ref(false);
const simConfigData = ref(null);
const simConfigEditable = ref(false);
const simConfigFormRef = ref(null);
const simConfigForm = reactive({
  updateIntervalSeconds: null,
  routeTimeoutSeconds: null,
  holdDownSeconds: null,
  maxMetric: null,
  splitHorizonMode: null,
  enableTriggeredUpdates: false
});

async function openSimConfigDialog() {
  const simId = currentSimulationId.value;
  if (!simId) {
    ElMessage.info('当前没有活动仿真 ID，无法查看配置');
    return;
  }

  if (!simRunning.value) {
    ElMessage.info('当前仿真未在运行或已结束，无法查看配置（配置存在于内存中，仅对运行中的仿真可用）');
    return;
  }

  simConfigDialogVisible.value = true;
  simConfigLoading.value = true;
  simConfigData.value = null;
  try {
    const resp = await http.get(`/api/simulations/${simId}/config`);
    simConfigData.value = resp?.data ?? null;
      // 初始化表单值（用于修改）
      const d = simConfigData.value || {};
      simConfigForm.updateIntervalSeconds = d.updateIntervalSeconds ?? d.update_interval_seconds ?? null;
      simConfigForm.routeTimeoutSeconds = d.routeTimeoutSeconds ?? d.route_timeout_seconds ?? null;
      simConfigForm.holdDownSeconds = d.holdDownSeconds ?? d.hold_down_seconds ?? null;
      simConfigForm.maxMetric = d.maxMetric ?? d.max_metric ?? null;
      simConfigForm.splitHorizonMode = d.splitHorizonMode ?? d.split_horizon_mode ?? null;
  simConfigForm.enableTriggeredUpdates = (d.enableTriggeredUpdates ?? d.enable_triggered_updates) !== undefined ? !!(d.enableTriggeredUpdates ?? d.enable_triggered_updates) : false;
    addActionRecord(`已获取仿真 ${simId} 的配置`);
  } catch (err) {
    console.error('fetch sim config error', err);
    ElMessage.error('获取仿真配置失败：' + (err?.response?.data?.message || err.message || '网络错误'));
  } finally {
    simConfigLoading.value = false;
  }
}

function enterEditSimConfig() {
  if (!simConfigData.value) {
    ElMessage.info('当前无配置可修改');
    return;
  }
  simConfigEditable.value = true;
}

function cancelEditSimConfig() {
  // 恢复为后端数据
  const d = simConfigData.value || {};
  simConfigForm.updateIntervalSeconds = d.updateIntervalSeconds ?? d.update_interval_seconds ?? null;
  simConfigForm.routeTimeoutSeconds = d.routeTimeoutSeconds ?? d.route_timeout_seconds ?? null;
  simConfigForm.holdDownSeconds = d.holdDownSeconds ?? d.hold_down_seconds ?? null;
  simConfigForm.maxMetric = d.maxMetric ?? d.max_metric ?? null;
  simConfigForm.splitHorizonMode = d.splitHorizonMode ?? d.split_horizon_mode ?? null;
  simConfigForm.enableTriggeredUpdates = (d.enableTriggeredUpdates ?? d.enable_triggered_updates) !== undefined ? !!(d.enableTriggeredUpdates ?? d.enable_triggered_updates) : false;
  simConfigEditable.value = false;
}

async function saveSimConfig() {
  const simId = currentSimulationId.value;
  if (!simId) {
    ElMessage.warning('无活动仿真 ID，无法保存');
    return;
  }

  // 简单校验：数字字段转换为整数
  const payload = {
    updateIntervalSeconds: simConfigForm.updateIntervalSeconds !== null ? Number(simConfigForm.updateIntervalSeconds) : undefined,
    routeTimeoutSeconds: simConfigForm.routeTimeoutSeconds !== null ? Number(simConfigForm.routeTimeoutSeconds) : undefined,
    holdDownSeconds: simConfigForm.holdDownSeconds !== null ? Number(simConfigForm.holdDownSeconds) : undefined,
    maxMetric: simConfigForm.maxMetric !== null ? Number(simConfigForm.maxMetric) : undefined,
    splitHorizonMode: simConfigForm.splitHorizonMode ?? undefined,
    enableTriggeredUpdates: simConfigForm.enableTriggeredUpdates !== null ? !!simConfigForm.enableTriggeredUpdates : undefined
  };

  // 移除 undefined 字段（只发送用户明确设置的字段）
  Object.keys(payload).forEach(k => { if (payload[k] === undefined) delete payload[k]; });

  try {
    const resp = await http.put(`/api/simulations/${simId}/config`, payload);
    ElMessage.success('仿真配置已保存');
    addActionRecord(`已更新仿真 ${simId} 的配置`);

    // 后端可能返回新的配置对象，或返回 204/empty。优先使用后端返回的主体。
    let newData = resp?.data ?? null;
    if (!newData) {
      try {
        const resp2 = await http.get(`/api/simulations/${simId}/config`);
        newData = resp2?.data ?? null;
      } catch (getErr) {
        console.warn('fetch after save failed, will fall back to sent payload', getErr);
        newData = null;
      }
    }

    if (!newData) {
      // 如果后端既没有返回也无法获取，回退到我们发送的 payload，以便界面立即反映用户修改
      newData = { ...(simConfigData.value || {}), ...payload };
    }

    // 更新本地显示数据及表单值
    simConfigData.value = newData;
    const d = simConfigData.value || {};
    simConfigForm.updateIntervalSeconds = d.updateIntervalSeconds ?? d.update_interval_seconds ?? null;
    simConfigForm.routeTimeoutSeconds = d.routeTimeoutSeconds ?? d.route_timeout_seconds ?? null;
    simConfigForm.holdDownSeconds = d.holdDownSeconds ?? d.hold_down_seconds ?? null;
    simConfigForm.maxMetric = d.maxMetric ?? d.max_metric ?? null;
    simConfigForm.splitHorizonMode = d.splitHorizonMode ?? d.split_horizon_mode ?? null;
  simConfigForm.enableTriggeredUpdates = (d.enableTriggeredUpdates ?? d.enable_triggered_updates) !== undefined ? !!(d.enableTriggeredUpdates ?? d.enable_triggered_updates) : false;

    simConfigEditable.value = false;
  } catch (err) {
    console.error('saveSimConfig error', err);
    ElMessage.error('保存仿真配置失败：' + (err?.response?.data?.message || err.message || '网络错误'));
  }
}

// 格式化仿真配置以便在 UI 中友好显示
const simConfigView = computed(() => {
  const data = simConfigData.value || {};
  const defaults = {
    updateIntervalSeconds: 30,
    routeTimeoutSeconds: 180,
    holdDownSeconds: 180,
    maxMetric: 16,
    splitHorizonMode: 'POISON_REVERSE', // backend default shown as SplitHorizonMode.POISON_REVERSE
    enableTriggeredUpdates: true
  };

  function shortEnum(v) {
    if (!v && v !== 0) return '';
    const s = String(v);
    if (s.includes('.')) return s.split('.').pop();
    return s;
  }

  function splitHorizonLabel(code) {
    const c = String(code || '').toUpperCase();
    if (c === 'NONE') return 'NONE（不启用水平分割）';
    if (c === 'SPLIT_HORIZON') return 'SPLIT_HORIZON（启用水平分割）';
    if (c === 'POISON_REVERSE') return 'POISON_REVERSE（启用毒性逆转）';
    return code;
  }

  const updateIntervalSeconds = Number(data.updateIntervalSeconds ?? data.update_interval_seconds ?? defaults.updateIntervalSeconds) || defaults.updateIntervalSeconds;
  const routeTimeoutSeconds = Number(data.routeTimeoutSeconds ?? data.route_timeout_seconds ?? defaults.routeTimeoutSeconds) || defaults.routeTimeoutSeconds;
  const holdDownSeconds = Number(data.holdDownSeconds ?? data.hold_down_seconds ?? defaults.holdDownSeconds) || defaults.holdDownSeconds;
  const maxMetric = Number(data.maxMetric ?? data.max_metric ?? defaults.maxMetric) || defaults.maxMetric;
  const rawSplit = shortEnum(data.splitHorizonMode ?? data.split_horizon_mode ?? defaults.splitHorizonMode);
  const enableTriggeredUpdates = (data.enableTriggeredUpdates ?? data.enable_triggered_updates ?? defaults.enableTriggeredUpdates) === true || (data.enableTriggeredUpdates ?? data.enable_triggered_updates ?? defaults.enableTriggeredUpdates) === 'true';

  return {
    updateIntervalSeconds: { key: 'updateIntervalSeconds', value: updateIntervalSeconds, description: '路由更新间隔时间（秒）', default: defaults.updateIntervalSeconds },
    routeTimeoutSeconds: { key: 'routeTimeoutSeconds', value: routeTimeoutSeconds, description: '路由超时时间（秒）——超过此时间未收到更新的路由将被视为不可达', default: defaults.routeTimeoutSeconds },
    holdDownSeconds: { key: 'holdDownSeconds', value: holdDownSeconds, description: '抑制时间（秒）——在路由失效后，抑制该路由更新的时间', default: defaults.holdDownSeconds },
    maxMetric: { key: 'maxMetric', value: maxMetric, description: '最大度量值，RIP 中表示路由不可达的度量值', default: defaults.maxMetric },
    splitHorizonMode: { key: 'splitHorizonMode', value: rawSplit, label: splitHorizonLabel(rawSplit), description: '水平分割模式枚举', default: defaults.splitHorizonMode, defaultLabel: splitHorizonLabel(defaults.splitHorizonMode) },
    enableTriggeredUpdates: { key: 'enableTriggeredUpdates', value: !!enableTriggeredUpdates, description: '是否启用触发更新', default: defaults.enableTriggeredUpdates }
  };
});

async function addNode(pos, metadata = '') {
  // 在添加节点时，允许在本地画布添加节点，即使当前没有激活拓扑
  const topologyId = getActiveTopologyId();

  // 使用顺序ID - 修改
  const currentNumber = nodeCounter.value;
  const tmpId = `tmp_${currentNumber}`;
  const clientRouterId = currentNumber; // 使用顺序数字作为路由器ID
  const nodeLabel = `R${currentNumber}`; // 生成标签 R1, R2, R3...
  
  // 添加到画布
  cy.add({ 
    group: 'nodes', 
    data: { 
      id: tmpId, 
      routerId: clientRouterId, 
      __tmp: true, 
      label: nodeLabel, 
      metric: 1, 
      metadata 
    }, 
    position: pos 
  });
  
  // 计数器加1 - 新增
  nodeCounter.value++;
  
  cy.elements().unselect();
  connectSource = null;
  currentMode.value = 'select';
  emits('topology-changed');
  addActionRecord(`本地添加临时节点 ${nodeLabel}`);
  // 如果存在激活拓扑，则尝试同步到后端；否则仅在本地保留临时节点，等待用户保存当前画布为新拓扑
  if (!topologyId) {
    addActionRecord(`本地添加临时节点（未保存到后端） ${nodeLabel}`);
    ElMessage.info('已在本地添加节点（尚未保存到拓扑）。请保存或创建拓扑以持久化。');
    return { local: true, id: tmpId, routerId: clientRouterId };
  }

  const payload = {
    routerId: clientRouterId,
    x: typeof pos.x === 'number' ? pos.x : 0,
    y: typeof pos.y === 'number' ? pos.y : 0,
    metadata: metadata || null
  };

  try {
    const resp = await http.post(`/api/topologies/${topologyId}/routers`, payload);
    const created = resp?.data || {};
    const backendId = created.id ?? created._id ?? null;
    const realRouterId = created.routerId ?? created.router_id ?? clientRouterId;

    // 更新节点数据
    const node = cy.getElementById(tmpId);
    if (node && node.length) {
      node.data('routerId', realRouterId);
      if (backendId !== null && backendId !== undefined) node.data('backendId', backendId);
      if (created.topologyId !== undefined) node.data('topologyId', created.topologyId);
      if (created.topology_id !== undefined) node.data('topologyId', created.topology_id);
      node.data('__tmp', false);
      node.data('metadata', created.metadata ?? payload.metadata ?? null);

      // 保持标签为 R1, R2, R3... 格式 - 修改
      node.data('label', nodeLabel);

      if (typeof created.x === 'number' && typeof created.y === 'number') {
        node.position({ x: created.x, y: created.y });
        node.data('x', created.x);
        node.data('y', created.y);
      } else {
        const p = node.position();
        node.data('x', typeof p.x === 'number' ? p.x : (payload.x || 0));
        node.data('y', typeof p.y === 'number' ? p.y : (payload.y || 0));
      }

      try { node.data('justCreatedAt', Date.now()); } catch (e) { /* ignore */ }
    }

    addActionRecord(`后端创建路由成功: ${nodeLabel}`);
    ElMessage.success('路由创建成功');
    return created;
  } catch (err) {
    console.error('create router API error', err);

    // 回滚：移除临时节点并恢复计数器 - 修改
    try {
      const node = cy.getElementById(tmpId);
      if (node && node.length) cy.remove(node);
      nodeCounter.value--; // 回滚计数器
      emits('topology-changed');
      addActionRecord(`已回滚临时节点 ${nodeLabel}`);
    } catch (e) {
      console.warn('回滚临时节点失败', e);
    }

    ElMessage.error('添加路由失败：' + (err?.response?.data?.message || err.message || '网络错误'));
    throw err;
  }
}

async function handleConnectMode(node) {
  if (!connectSource) {
    connectSource = node;
    node.select();
    return;
  }

  // 点击同一个节点：取消选择
  if (connectSource.id() === node.id()) {
    connectSource.unselect();
    connectSource = null;
    return;
  }

  
  const fromElId = connectSource.id();
  const toElId = node.id();

  
  const fromRouterId = connectSource.data('routerId') ?? fromElId;
  const toRouterId = node.data('routerId') ?? toElId;

  
  const existing = cy.edges().filter(e => e.data('source') === fromElId && e.data('target') === toElId);
  if (existing && existing.length > 0) {
    
    connectSource.unselect();
    connectSource = null;
    return;
  }

  const tmpEdgeId = `tmp_edge_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
  
  cy.add({ group: 'edges', data: { id: tmpEdgeId, source: fromElId, target: toElId, __tmp: true, cost: 1, routerFrom: fromRouterId, routerTo: toRouterId } });
  connectSource.unselect();
  connectSource = null;
  emits('topology-changed');
  addActionRecord(`本地添加临时连接 ${fromRouterId} → ${toRouterId}`);

  const topologyId = getActiveTopologyId();
  if (!topologyId) {
    ElMessage.warning('请先选择或创建拓扑，然后再创建链路');
    // 回滚临时边
    try { const e = cy.getElementById(tmpEdgeId); if (e && e.length) cy.remove(e); } catch (e) { }
    return;
  }

  // 询问用户输入链路 cost（默认为 1），如果取消则回滚临时边
  try {
    const { value } = await ElMessageBox.prompt('请输入链路 cost（正整数）', '设置链路代价', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      inputPlaceholder: '1',
      inputPattern: /^[0-9]+$/,
      inputErrorMessage: '请输入一个非负整数',
      inputValue: '1'
    });

    const parsedCost = Number(value ?? '1') || 1;
    const payload = { from: String(fromRouterId), to: String(toRouterId), cost: parsedCost };

    try {
      const resp = await http.post(`/api/topologies/${topologyId}/links`, payload);
      const created = resp?.data || {};
      const realId = created.id ?? created.linkId ?? `${fromRouterId}-${toRouterId}`;

      try {
        const tmp = cy.getElementById(tmpEdgeId);
        if (tmp && tmp.length) cy.remove(tmp);
      } catch (e) {
        console.warn('remove tmp edge failed', e);
      }

      cy.add({ group: 'edges', data: { id: realId, source: fromElId, target: toElId, cost: payload.cost, routerFrom: fromRouterId, routerTo: toRouterId } });
      emits('topology-changed');
      addActionRecord(`后端创建链路成功: ${realId}`);
      ElMessage.success('创建链路成功');
    } catch (err) {
      console.error('create link API error', err);
      addActionRecord('创建链路失败（后端）');
      ElMessage.error('创建链路失败：' + (err?.response?.data?.message || err.message || '网络错误'));

      try {
        const tmp = cy.getElementById(tmpEdgeId);
        if (tmp && tmp.length) cy.remove(tmp);
        emits('topology-changed');
        addActionRecord(`已回滚临时连接 ${fromRouterId} → ${toRouterId}`);
      } catch (e) {
        console.warn('回滚临时连接失败', e);
      }
    }
  } catch (promptErr) {
    // 用户取消或输入校验失败，回滚临时边
    try {
      const tmp = cy.getElementById(tmpEdgeId);
      if (tmp && tmp.length) cy.remove(tmp);
      emits('topology-changed');
      addActionRecord(`已取消创建链路 ${fromRouterId} → ${toRouterId}`);
    } catch (e) {
      console.warn('rollback tmp edge after cancel failed', e);
    }
    return;
  }
}

// 查看快照方法
function viewSnapshot(nodeId = null) {
  // 保存选中的节点信息到 localStorage
  if (nodeId) {
    const node = cy.getElementById(nodeId);
    if (node && node.isNode()) {
      const nodeData = node.json();
      const routerInfo = {
        routerId: nodeData.data?.routerId || nodeData.data?.id,
        label: nodeData.data?.label,
        topologyId: getActiveTopologyId()
      };
      localStorage.setItem('selectedRouterForSnapshot', JSON.stringify(routerInfo));
    }
  } else {
    // 没有选中节点时清除之前的选中信息
    localStorage.removeItem('selectedRouterForSnapshot');
  }
  
  // 跳转到快照分析页面
  window.location.href = '/1';
  
  contextMenu.value.visible = false;
}

function deleteNode(id) {
  const node = cy.getElementById(id);
  if (node) {
    addActionRecord(`删除节点 ${id}`);
    node.remove();
    emits("topology-changed");
    if (selectedNode.value && selectedNode.value.data.id === id) {
      selectedNode.value = null;
    }
  }
  contextMenu.value.visible = false;
}

async function clearAll() {
  const topoId = getActiveTopologyId();
  // 如果存在激活拓扑，倾向于在后端清空其 routers/links 而不是取消选择拓扑
  if (topoId) {
    try {
      await ElMessageBox.confirm(
        `确认要清空拓扑内的所有路由和链路吗？此操作会删除拓扑内的所有节点与连接，但不会删除拓扑本身。`,
        '清空拓扑内容',
        { confirmButtonText: '清空', cancelButtonText: '取消', type: 'warning' }
      );

      addActionRecord(`开始清空拓扑 ${topoId} 的所有路由与链路`);

      // 构造空 routers/links payload，同时保留 name/description（若 activeTopology 可用）
      const payload = {
        name: activeTopology.value?.name ?? localStorage.getItem('activeTopologyName') ?? '',
        description: activeTopology.value?.description ?? '' ,
        routers: [],
        links: []
      };

      const resp = await http.put(`/api/topologies/${topoId}`, payload);
      const res = resp?.data ?? null;

      // 仅在后端成功返回或无异常时清空前端画布
      cy.elements().remove();
      emits('topology-changed');
      selectedNode.value = null;
      contextMenu.value.visible = false;
      nodeCounter.value = 1;

      // 更新本地 activeTopology（如果后端有返回最新对象）
      if (res) {
        activeTopology.value = res;
        try { if (res.name) localStorage.setItem('activeTopologyName', String(res.name)); } catch (e) { /* ignore */ }
      }

      addActionRecord(`已清空拓扑 ${topoId} 的内容`);
      ElMessage.success('已清空当前拓扑的所有节点与链路');
      return;
    } catch (err) {
      const isCancel = err === 'cancel' || err === 'close' || err?.action === 'cancel' || err?.action === 'close';
      if (isCancel) {
        addActionRecord('已取消清空拓扑操作');
        return;
      }
      console.error('clearAll (backend clear) error', err);
      ElMessage.error('清空拓扑失败：' + (err?.response?.data?.message || err.message || '网络错误'));
      addActionRecord(`清空拓扑 ${topoId} 失败`);
      return;
    }
  }

  // 无激活拓扑时仅在前端清空画布
  addActionRecord('清空本地拓扑图 (无激活拓扑)');
  cy.elements().remove();
  emits('topology-changed');
  selectedNode.value = null;
  contextMenu.value.visible = false;
  nodeCounter.value = 1;
}

// 仅在前端清空画布（不与后端交互、也不弹窗）
function clearLocalCanvas() {
  addActionRecord('本地清空拓扑画布（无后端交互）');
  try {
    cy && cy.elements() && cy.elements().remove();
  } catch (e) { console.warn('clearLocalCanvas remove elements failed', e); }
  emits('topology-changed');
  selectedNode.value = null;
  contextMenu.value.visible = false;
  nodeCounter.value = 1;
}

function addNodeAtContext() {
  const pos = contextMenu.value.cyPos;
  if (pos) {
    addNode(pos);
    contextMenu.value.visible = false;
  }
}

function setAsSource(id) {
  connectSource = cy.getElementById(id);
  connectSource.select();
  currentMode.value = "connect";
  contextMenu.value.visible = false;
}

function viewNode(id) {
  const node = cy.getElementById(id);
  emits("node-selected", node.json());
  contextMenu.value.visible = false;
}

// 显示属性弹窗
function showPropertyModal(nodeId) {
  if (!nodeId) return;
  
  const node = cy.getElementById(nodeId);
  if (node && node.isNode()) {
    const nodeJson = node.json();
    currentNodeData.value = {
      data: { ...nodeJson.data },
      position: { ...nodeJson.position }
    };
   
    currentNodeData.value.data.routerId = nodeJson.data?.routerId ?? nodeJson.data?.id ?? '';
    

    propertyModal.value.visible = true;
    propertyModal.value.nodeId = nodeId;
  }
  
  contextMenu.value.visible = false;
}

// 关闭属性弹窗
function closePropertyModal() {
  propertyModal.value.visible = false;
  propertyModal.value.nodeId = null;
}

// 处理标签变化
function handleLabelChange() {
  const node = cy.getElementById(propertyModal.value.nodeId);
  if (node) {
    node.data('label', currentNodeData.value.data.label);
  }
}

// 处理指标变化
function handleMetricChange() {
}

// 保存属性
function saveProperties() {
  const node = cy.getElementById(propertyModal.value.nodeId);
  if (node) {
    node.data('label', currentNodeData.value.data.label);
    node.data('metric', Number(currentNodeData.value.data.metric));
    node.style("label", currentNodeData.value.data.label);
    
    emits("topology-changed");
    addActionRecord(`更新节点 ${node.id()} 属性`);
    closePropertyModal();
  }
}

// 视图控制函数
function zoomIn() {
  cy.zoom(cy.zoom() * 1.2);
}

function zoomOut() {
  cy.zoom(cy.zoom() / 1.2);
}

function fitToScreen() {
  cy.fit();
}

function centerContent() {
  cy.center();
}

function updateNode(updatedNode) {
  const id = updatedNode.data.id;
  const node = cy.getElementById(id);
  if (node && node.isNode()) {
    node.data("label", updatedNode.data.label);
    node.data("metric", updatedNode.data.metric);
    node.style("label", updatedNode.data.label);
    emits("topology-changed");
  }
}

function exportJson() {
  if (nodeCount.value === 0 || !getActiveTopologyId()) {
    ElMessage.warning('当前没有可导出的拓扑或画布为空');
    return;
  }
  const data = cy.json();
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = "topology.json";
  a.click();
  URL.revokeObjectURL(url);
  addActionRecord('导出拓扑数据');
}

// 更新当前拓扑
async function saveActiveTopology() {
  const topoId = getActiveTopologyId();
  if (!topoId) {
    // 如果画布有内容，弹出创建拓扑对话，以便用户输入名称（必填）并将画布作为新拓扑保存
    try {
      const hasNodes = cy && cy.nodes && cy.nodes().length > 0;
      if (hasNodes) {
        createFormData.name = '';
        createFormData.description = '';
        createModalMode.value = 'preserve';
        createModalVisible.value = true;
        addActionRecord('无激活拓扑：弹出创建拓扑对话以保存当前画布');
        return;
      }
    } catch (e) { /* ignore cytoscape errors */ }

    ElMessage.warning('当前没有激活的拓扑 ID，无法保存。请先选择或创建拓扑。');
    return;
  }

  try {
    // 构建 routers 列表
    const routers = cy.nodes().map(n => {
      const d = n.data();
      const p = n.position();
      const routerId = String(d.routerId ?? (d.id ?? n.id())).replace(/^router_/, '');
      return {
        routerId: routerId,
        x: typeof p.x === 'number' ? p.x : 0,
        y: typeof p.y === 'number' ? p.y : 0,
        
        metadata: (d.metadata === undefined) ? null : d.metadata
      };
    });

    // 构建 links 列表
    const links = cy.edges().map(e => {
      const d = e.data();
      
      const from = String(d.routerFrom ?? (e.source && e.source().data ? e.source().data('routerId') : null) ?? (e.source && e.source().id ? e.source().id().replace(/^router_/, '') : '')).replace(/^router_/, '');
      const to = String(d.routerTo ?? (e.target && e.target().data ? e.target().data('routerId') : null) ?? (e.target && e.target().id ? e.target().id().replace(/^router_/, '') : '')).replace(/^router_/, '');
      return { from, to, cost: Number(d.cost ?? 1) };
    });

    const payload = {
      name: activeTopology.value?.name ?? localStorage.getItem('activeTopologyName') ?? '',
      description: activeTopology.value?.description ?? '',
      routers,
      links
    };

    
    try { console.log('saveActiveTopology payload', payload); } catch (e) {}

    
    const badRouters = routers.filter(r => !r.routerId || String(r.routerId).trim() === '');
    if (badRouters.length > 0) {
      ElMessage.error('存在未设置 routerId 的节点，无法保存，请为这些节点设置 routerId 后重试');
      addActionRecord('保存失败：存在未设置 routerId 的节点');
      return;
    }
   
    const seen = new Set();
    const dupes = [];
    routers.forEach(r => {
      if (seen.has(r.routerId)) dupes.push(r.routerId);
      else seen.add(r.routerId);
    });
    if (dupes.length > 0) {
      ElMessage.error('存在重复的 routerId: ' + Array.from(new Set(dupes)).join(', '));
      addActionRecord('保存失败：存在重复的 routerId');
      return;
    }

    
    const routerSet = new Set(routers.map(r => String(r.routerId)));
    const missingRefs = [];
    links.forEach(l => {
      if (!routerSet.has(String(l.from))) missingRefs.push(l.from);
      if (!routerSet.has(String(l.to))) missingRefs.push(l.to);
    });
    if (missingRefs.length > 0) {
      ElMessage.error('链路引用了不存在的路由器 ID: ' + Array.from(new Set(missingRefs)).join(', '));
      addActionRecord('保存失败：链路引用了不存在的路由器 ID');
      return;
    }

    const resp = await http.put(`/api/topologies/${topoId}`, payload);
    const res = resp?.data;
    ElMessage.success('拓扑已保存');
    addActionRecord(`已保存拓扑 ${topoId}`);
    if (res) {
      activeTopology.value = res;
      try { if (res.name) localStorage.setItem('activeTopologyName', String(res.name)); } catch (e) {}
    }
    return res;
  } catch (err) {
    console.error('saveActiveTopology error', err);
    ElMessage.error('保存拓扑失败：' + (err?.response?.data?.message || err.message || '网络错误'));
    addActionRecord('保存拓扑失败');
    throw err;
  }
}

function importJson() {
  fileInput.value?.click();
}

// 确认创建对话的统一入口：根据模式分流
async function onConfirmCreateTopology() {
  if (createModalMode.value === 'preserve') {
    // 将当前画布保存为后端拓扑，但不清空画布
    try {
      await saveCanvasAsTopology();
    } catch (e) {
      console.error('saveCanvasAsTopology failed', e);
    }
  } else {
    // 常规创建（清空画布）
    try {
      await createTopology();
    } catch (e) {
      console.error('createTopology failed', e);
    }
  }
}

// 新函数：把当前画布作为拓扑保存到后端，但保留画布内容（不清空）
async function saveCanvasAsTopology() {
  if (!createFormData.name || !createFormData.name.trim()) {
    try { await createFormRef.value.validate(); } catch (e) { return; }
  }

  addActionRecord('开始将当前画布保存为拓扑（不清空）');

  const name = createFormData.name.trim();
  const description = createFormData.description ?? '';

  // 从画布收集 routers/links
  const routers = [];
  const links = [];
  try {
    if (cy && cy.nodes) {
      cy.nodes().forEach(n => {
        const d = n.data() || {};
        const p = (n.position && typeof n.position === 'function') ? n.position() : { x: d.x ?? 0, y: d.y ?? 0 };
        const rawId = d.routerId ?? d.router_id ?? d.label ?? n.id();
        const routerId = String(rawId ?? '').replace(/^router_/, '');
        routers.push({ routerId, x: typeof p.x === 'number' ? p.x : 0, y: typeof p.y === 'number' ? p.y : 0 });
      });
    }

    if (cy && cy.edges) {
      cy.edges().forEach(e => {
        const d = e.data() || {};
        const srcEl = (e.source && typeof e.source === 'function') ? e.source().id() : (d.source ?? d.from);
        const tgtEl = (e.target && typeof e.target === 'function') ? e.target().id() : (d.target ?? d.to);
        const srcNode = cy.getElementById(srcEl);
        const tgtNode = cy.getElementById(tgtEl);
        const from = srcNode && srcNode.length ? (srcNode.data('routerId') ?? srcNode.data('label') ?? srcEl) : (d.from ?? d.source);
        const to = tgtNode && tgtNode.length ? (tgtNode.data('routerId') ?? tgtNode.data('label') ?? tgtEl) : (d.to ?? d.target);
        const cost = Number(d.cost ?? 1);
        if (from && to) links.push({ from: String(from).replace(/^router_/, ''), to: String(to).replace(/^router_/, ''), cost });
      });
    }
  } catch (e) {
    console.warn('collect routers/links failed', e);
  }

  const payload = { name, description, routers, links };

  try {
    const resp = await http.post('/api/topologies', payload);
    const res = resp?.data;

    createModalVisible.value = false;

    const returnedTopoId = res?.id ?? res?.topologyId ?? res?.topology_id ?? null;
    if (returnedTopoId) {
      try { setActiveTopologyId(returnedTopoId); } catch (e) { console.warn('保存 activeTopologyId 失败', e); }
    }

    // 不清空画布：仅更新 activeTopology 最小信息以标记已保存
    try {
      activeTopology.value = { id: returnedTopoId ?? null, name: name, description: description };
      try { if (name) localStorage.setItem('activeTopologyName', String(name)); } catch (e) {}
    } catch (e) { /* ignore */ }

    addActionRecord(`已将画布保存为拓扑 ${name || returnedTopoId || ''}`);
    ElMessage.success('画布已保存为拓扑');
    return res;
  } catch (err) {
    console.error('saveCanvasAsTopology error', err);
    ElMessage.error('保存拓扑失败：' + (err?.response?.data?.message || err.message || '网络错误'));
    throw err;
  }
}

async function createTopology() {
  
  if (!createFormData.name || !createFormData.name.trim()) {
    
    try {
      await createFormRef.value.validate();
    } catch (e) {
      return;
    }
  }

  addActionRecord('开始创建拓扑（清空画布）');

  const name = createFormData.name.trim();
  const description = createFormData.description ?? '';

  try {
    const payload = { name, description, routers: [], links: [] };
    const resp = await http.post('/api/topologies', payload);
    const res = resp?.data;

    createModalVisible.value = false;

    // 清空当前画布（创建拓扑时需要清空）
    try { clearLocalCanvas(); } catch (e) { /* ignore */ }

    if (res) {
      // 如果后端返回元素则渲染
      if (res.elements) {
        cy.add(res.elements);
      } else if (res.nodes || res.edges) {
        cy.add(res);
      }

      cy.resize();
      cy.center();
      emits('topology-changed');
      addActionRecord(`创建拓扑 ${name || res.id || '成功'}`);

      const returnedTopoId = res.id ?? res.topologyId ?? res.topology_id;
      if (returnedTopoId) {
        try { setActiveTopologyId(returnedTopoId); } catch (e) { console.warn('保存 activeTopologyId 失败', e); }

        try {
          if (res.name) {
            activeTopology.value = res;
            try { localStorage.setItem('activeTopologyName', String(res.name)); } catch (e) {}
          }

          try {
            const idx = topologyList.value.findIndex(t => String(t.id) === String(returnedTopoId));
            if (idx >= 0) {
              topologyList.value[idx] = { ...topologyList.value[idx], ...(res.name ? { name: res.name } : {}), ...(res.description ? { description: res.description } : {}) };
            } else {
              topologyList.value.unshift({ id: returnedTopoId, name: res.name ?? name, description: res.description ?? '' });
            }
          } catch (e) { /* ignore */ }
        } catch (e) { /* ignore */ }
      }
      updateStatistics();
    }
  } catch (err) {
    console.error('createTopology API error', err);
    addActionRecord('创建拓扑失败（后端）');

    try {
      createModalVisible.value = false;
      clearLocalCanvas();
      const id = `topo_${Date.now()}`;
      const pos = { x: cy.width() / 2, y: cy.height() / 2 };
      cy.add({ group: 'nodes', data: { id, label: name || id, metric: 1 }, position: pos });
      emits('topology-changed');
      addActionRecord('已创建本地拓扑（回退）');

      try { setActiveTopologyId(id); } catch (e) { console.warn('保存回退拓扑 id 失败', e); }
      updateStatistics();
    } catch (e) {
      console.error('createTopology fallback error', e);
    }
  }
}

function handleFileChange(e) {
  const file = e.target.files[0];
  if (!file) return;
  const reader = new FileReader();
  reader.onload = (event) => {
    try {
      // 解析 JSON 内容
      const text = event.target.result;
      let json = null;
      try {
        json = JSON.parse(text);
      } catch (pe) {
        console.error('JSON parse error during import', pe);
        ElMessage.error('导入失败：JSON 解析错误，请检查文件格式');
        return;
      }

      // 不再清空画布：将导入内容合并到当前画布中（跳过已有 id 的元素）
      const elements = json.elements ?? (json.nodes || null);

      if (!elements) {
        // 如果没有标准的 elements/nodes 字段，尝试从顶层解析 nodes/edges
        const nodesArr = json.nodes ?? (json.elements && json.elements.nodes) ?? null;
        const edgesArr = json.edges ?? (json.elements && json.elements.edges) ?? null;
        if (!nodesArr && !edgesArr) {
          throw new Error('导入文件缺少可识别的 nodes 或 edges 字段，无法合并');
        }
        // 将 nodes/edges 合并
        mergeAndAddElements(nodesArr || [], edgesArr || []);
      } else {
        // elements 可能是 { nodes: [...], edges: [...] } 或单纯的数组
        if (Array.isArray(elements)) {
          // 如果是数组，认为是 nodes 列表
          mergeAndAddElements(elements, []);
        } else {
          const nodesArr = elements.nodes ?? [];
          const edgesArr = elements.edges ?? [];
          mergeAndAddElements(nodesArr, edgesArr);
        }
      }

      // 导入后重新计算最大节点ID - 新增
      const nodes = cy.nodes();
      let maxId = 0;
      nodes.forEach(node => {
        const routerId = node.data('routerId');
        if (routerId && typeof routerId === 'number') {
          maxId = Math.max(maxId, routerId);
        }
      });
      nodeCounter.value = maxId + 1;

      emits("topology-changed");
      addActionRecord('导入拓扑数据');
    } catch (err) {
      ElMessage.error('导入失败：' + (err?.message || '无效 JSON 文件'));
      console.error('handleFileChange error', err);
    }
  };
  reader.readAsText(file);
  e.target.value = "";
}

// 将 nodes/edges 与当前画布合并并添加：跳过已存在的元素 id，确保边的端点存在
function mergeAndAddElements(nodesArr, edgesArr) {
  try {
    const toAddNodes = [];
    const toAddEdges = [];

    // 规范节点为 cytoscape 节点格式 { data: { id, ... }, position? }
    (nodesArr || []).forEach(n => {
      const id = (n.data && n.data.id) || n.id || (n.data && n.data.routerId) ? (n.data && n.data.id ? String(n.data.id) : (n.id ? String(n.id) : String(n.data.routerId))) : null;
      const finalId = id ? String(id) : (n.id || null);
      if (finalId && cy.getElementById(finalId) && cy.getElementById(finalId).length) {
        // 已存在则跳过
        return;
      }
      // Ensure node is in proper format
      if (n.group === 'nodes' || (n.data && (n.data.id || n.data.routerId))) {
        toAddNodes.push(n);
      } else if (n.id || n.data) {
        toAddNodes.push({ data: n.data || { id: n.id }, position: n.position });
      } else {
        // unknown shape, attempt to use as data.id
        toAddNodes.push({ data: n });
      }
    });

    // 处理边，确保 source/target 存在或会被添加
    (edgesArr || []).forEach(e => {
      const eid = e.data?.id ?? e.id ?? null;
      const src = e.data?.source ?? e.source ?? e.from ?? e.routerFrom ?? null;
      const tgt = e.data?.target ?? e.target ?? e.to ?? e.routerTo ?? null;
      const already = eid && cy.getElementById(eid) && cy.getElementById(eid).length;
      if (already) return;
      // only add if both endpoints exist in graph or are in toAddNodes
      const srcExists = (src && (cy.getElementById(String(src)).length || toAddNodes.some(n => (n.data && (String(n.data.id) === String(src) || String(n.data.routerId) === String(src))))));
      const tgtExists = (tgt && (cy.getElementById(String(tgt)).length || toAddNodes.some(n => (n.data && (String(n.data.id) === String(tgt) || String(n.data.routerId) === String(tgt))))));
      if (!srcExists || !tgtExists) {
        // skip edges that reference missing nodes
        return;
      }
      if (e.group === 'edges' || (e.data && (e.data.source || e.data.target))) {
        toAddEdges.push(e);
      } else if (src && tgt) {
        toAddEdges.push({ data: { id: eid ?? `edge_${src}_${tgt}_${Date.now()}`, source: src, target: tgt, cost: e.cost ?? e.data?.cost ?? 1 } });
      }
    });

    // 批量添加
    if (toAddNodes.length || toAddEdges.length) {
      try {
        if (toAddNodes.length && toAddEdges.length) cy.add({ nodes: toAddNodes, edges: toAddEdges });
        else if (toAddNodes.length) cy.add(toAddNodes);
        else if (toAddEdges.length) cy.add(toAddEdges);
      } catch (e) {
        console.warn('mergeAndAddElements cy.add failed', e);
      }

      // 如果新添加的节点含位置信息则保持当前视图，否则尝试 fit
      const hasPos = toAddNodes.some(n => n && (n.position && typeof n.position.x === 'number' && typeof n.position.y === 'number'));
      try {
        if (hasPos) { cy.resize(); cy.center(); }
        else { cy.layout({ name: 'grid', animate: false }).run(); cy.resize(); cy.fit(); }
      } catch (e) { console.warn('layout after merge failed', e); }
    }
  } catch (e) {
    console.error('mergeAndAddElements error', e);
  }
}

function getNodeData(id) {
  const node = cy.getElementById(id);
  return node ? node.json() : null;
}

function getCurrentSimulationId() {
  return currentSimulationId.value;
}

function setCurrentSimulationId(id) {
  if (id) {
    currentSimulationId.value = String(id);
    try { localStorage.setItem('currentSimulationId', String(id)); } catch (e) { /* ignore */ }
  } else {
    currentSimulationId.value = null;
    try { localStorage.removeItem('currentSimulationId'); } catch (e) { /* ignore */ }
  }
}

defineExpose({ 
  setMode, 
  clear: clearAll, 
  updateNode, 
  removeNode: deleteNode, 
  exportJson, 
  importJson, 
  getNodeData,
  showPropertyModal,
  closePropertyModal,
  // expose topology id helpers
  getActiveTopologyId,
  setActiveTopologyId
  ,getCurrentSimulationId,
  setCurrentSimulationId
});
</script>

<style scoped>
:root {
  --bg-color: #f4f7fb;
  --card-bg: rgba(255,255,255,0.95);
  --primary: #0074D9;
  --muted: #6b7280;
  --accent: #00bcd4;
  --shadow: 0 8px 24px rgba(16,24,40,0.08);
  --radius: 12px;
  --sidebar-width: 280px;
}

.topology-container {
  position: relative;
  display: flex;
  flex-direction: column;
  height: 100vh;
  background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
  overflow: hidden;
}

/* 头部标题栏 */
.header-section {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 24px;
  background: var(--card-bg);
  border-bottom: 1px solid rgba(0,0,0,0.06);
  box-shadow: 0 2px 8px rgba(0,0,0,0.04);
}

.title-container .main-title {
  margin: 0;
  font-size: 24px;
  font-weight: 600;
  color: #1a1a1a;
}

.title-container .subtitle {
  margin: 4px 0 0 0;
  font-size: 14px;
  color: var(--muted);
}

/* 主内容区域 */
.main-content {
  display: flex;
  flex: 1;
  gap: 16px;
  padding: 16px;
  overflow: hidden;
 
  min-height: 0;
}

/* 左侧边栏 */
.left-sidebar {
  width: var(--sidebar-width);
  display: flex;
  flex-direction: column;
  gap: 16px;
  
  min-height: 0;
  overflow: auto;
}

/* 中央内容区域 */
.center-content {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-width: 0;
}

/* 右侧边栏 */
.right-sidebar {
  width: var(--sidebar-width);
  display: flex;
  flex-direction: column;
  gap: 16px;
}


.right-sidebar {
  max-height: calc(100vh - 120px); 
  overflow: auto;
}

/* 卡片通用样式 */
.mode-card,
.stats-card,
.quick-actions-card,
.selection-card,
.status-card,
.recent-actions-card {
  background: var(--card-bg);
  border-radius: var(--radius);
  padding: 20px;
  box-shadow: var(--shadow);
  border: 1px solid rgba(0,0,0,0.04);
}

.selection-card {
  min-height: 120px;
}

.card-title {
  margin: 0 0 16px 0;
  font-size: 16px;
  font-weight: 600;
  color: #1a1a1a;
  padding-bottom: 8px;
  border-bottom: 1px solid rgba(0,0,0,0.06);
}


.card-title {
  display: block;
  position: relative;
  z-index: 2;
  line-height: 1.25;
  margin-bottom: 12px;
}


.mode-card > .card-title + *,
.stats-card > .card-title + *,
.quick-actions-card > .card-title + *,
.selection-card > .card-title + *,
.recent-actions-card > .card-title + * {
  margin-top: 8px;
}


.action-list,
.node-details,
.action-list-container {
  margin-top: 6px;
}

/* 模式按钮 */
.mode-buttons {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.mode-btn {
  justify-content: flex-start;
  padding: 12px 16px;
  border-radius: 8px;
  transition: all 0.2s ease;
}

.mode-btn:hover {
  transform: translateX(4px);
}

/* 统计网格 */
.stats-grid {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 12px;
}

.stat-item {
  text-align: center;
  padding: 12px 8px;
  background: rgba(0,116,217,0.05);
  border-radius: 8px;
}

.stat-value {
  font-size: 20px;
  font-weight: 600;
  color: var(--primary);
}

.stat-label {
  font-size: 12px;
  color: var(--muted);
  margin-top: 4px;
}

/* 操作按钮 */
.action-buttons {
  display: flex;
  flex-direction: column;
  gap: 8px;
}


.quick-actions-card {
  /* 使卡片内部可以按列布局，便于把按钮区设为可滚动 */
  display: flex;
  flex-direction: column;
}

.quick-actions-card .action-buttons {
  flex: 1 1 auto;
  overflow-y: auto;
  max-height: 240px; /* 限制高度，超过显示滚动 */
  padding-right: 6px; /* 给滚动条留出空间 */
}

.quick-actions-card .action-buttons .el-button {
  width: 100%;
  text-align: left;
}

.action-btn {
  justify-content: flex-start;
  padding: 10px 16px;
  border-radius: 6px;
}

.mode-buttons,
.action-buttons {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
}


.mode-buttons .el-button,
.action-buttons .el-button {
  margin: 0 !important;
}


.info-card-bottom {
  background: var(--card-bg);
  border-radius: var(--radius);
  padding: 16px 20px;
  box-shadow: var(--shadow);
  border: 1px solid rgba(0,0,0,0.06);
  margin-top: 8px; /* 与画布间隔 */
  text-align: center; /* 添加这一行让文字居中 */
}

.info-card-bottom h4 {
  text-align: center;
  margin-bottom: 12px;
  padding-bottom: 8px;
  border-bottom: 1px solid rgba(0,0,0,0.05);
  color: #1a1a1a;
  font-size: 14px;
  font-weight: 600;
}

.info-card-bottom ul {
  margin: 0;
  padding-left: 0; /* 移除默认的左padding */
  color: var(--muted);
  font-size: 13px;
  line-height: 1.5;
  list-style: none; /* 移除默认的列表符号 */
  display: inline-block; /* 让ul内容居中 */
  text-align: left; /* 列表项内部文字左对齐 */
}

.info-card-bottom li {
  margin-bottom: 6px;
  position: relative;
  padding-left: 16px; /* 为自定义符号留出空间 */
}

/* 放大缩小按钮组 - 画布内部右下角 */
.zoom-controls-bottom {
  position: absolute;
  bottom: 20px;
  right: 20px;
  z-index: 40;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  background: rgba(255, 255, 255, 0.9);
  padding: 8px;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  border: 1px solid rgba(0, 0, 0, 0.1);
}

.info-card-bottom li:before {
  content: "•"; /* 使用圆点作为符号 */
  color: var(--primary);
  position: absolute;
  left: 0;
}

.zoom-controls-bottom .el-button-group {
  display: flex;
  flex-direction: column;
}

.zoom-btn {
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.zoom-level-bottom {
  font-size: 12px;
  color: var(--muted);
  font-weight: 500;
}


.zoom-level {
  padding: 6px 12px;
  background: var(--card-bg);
  border-radius: 6px;
  font-size: 12px;
  color: var(--muted);
  border: 1px solid rgba(0,0,0,0.06);
}

/* 拓扑画布容器 */
.cytoscape-wrapper {
  position: relative;
  flex: 1;
  background: var(--card-bg);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
  border: 1px solid rgba(0,0,0,0.06);
  z-index: 1;
}

.cytoscape-container {
  width: 100%;
  height: 100%;
  background: linear-gradient(45deg, #fafbfc 0%, #f0f4f8 100%);
}


.topology-overlay {
  position: absolute;
  
  top: 40px;
  left: 40px;
  z-index: 40;
  pointer-events: auto;
}
.overlay-card {
  width: 260px;
  padding: 12px;
  background: var(--card-bg);
  border-radius: 8px;
  box-shadow: var(--shadow);
  border: 1px solid rgba(0,0,0,0.06);
}
.overlay-card {
  opacity: 0.98;
}
.overlay-card .card-title {
  margin-bottom: 8px;
  font-size: 14px;
}

.canvas-overlay {
  position: absolute;
  top: 12px;
  left: 12px;
  pointer-events: none; 
}


.canvas-overlay > * {
  pointer-events: auto;
}

.mode-indicator {
  padding: 6px 12px;
  background: rgba(0,0,0,0.7);
  color: white;
  border-radius: 6px;
  font-size: 12px;
  font-weight: 500;
}

.mode-indicator.select { background: rgba(0,116,217,0.8); }
.mode-indicator.add { background: rgba(0,188,212,0.8); }
.mode-indicator.connect { background: rgba(40,167,69,0.8); }

/* 节点详情 */
.node-details {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-bottom: 16px;
}

.detail-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 0;
  border-bottom: 1px solid rgba(0,0,0,0.04);
}

.detail-item label {
  font-weight: 500;
  font-size: 13px;
  color: var(--muted);
}

.detail-value {
  font-size: 13px;
  color: #1a1a1a;
  font-family: monospace;
}

.node-actions {
  display: flex;
  gap: 8px;
}

/* 未选择节点状态 */
.empty-node-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 40px 20px;
  color: var(--muted);
  text-align: center;
}

.empty-node-state .el-icon {
  font-size: 48px;
  margin-bottom: 12px;
  opacity: 0.3;
}

.empty-node-state span {
  font-size: 14px;
  font-weight: 500;
  margin-bottom: 8px;
}

.empty-node-state p {
  font-size: 12px;
  margin: 0;
  opacity: 0.7;
}

/* 操作记录 */
.action-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  max-height: 200px;
  overflow-y: auto;
}

.action-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px;
  background: rgba(0,0,0,0.02);
  border-radius: 6px;
  font-size: 12px;
}

.action-icon {
  color: var(--primary);
  font-size: 12px;
}

.action-text {
  flex: 1;
  color: #1a1a1a;
}

.action-time {
  color: var(--muted);
  font-size: 11px;
}

/* 最近操作面板样式 */
.recent-actions-panel {
  background: var(--card-bg);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  border: 1px solid rgba(0,0,0,0.04);
  height: 100%;
  display: flex;
  flex-direction: column;
}


.recent-actions-panel {
  flex: 1 1 auto;
  min-height: 140px;
  max-height: calc(100vh - 220px);
  overflow: auto;
}

/* 仿真记录对话内部可滚动区域：保持对话头尾固定，仅中间区域滚动 */
.sim-records-body {
  max-height: calc(100vh - 260px);
  overflow: auto;
  padding: 12px 16px;
}

.panel-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px 20px 0 20px;
  margin-bottom: 16px;
}

.panel-header .card-title {
  margin: 0;
  border: none;
  padding: 0;
}

.close-btn {
  padding: 6px;
  border-radius: 6px;
}

.action-list-container {
  flex: 1;
  overflow-y: auto;
  padding: 0 20px;
  max-height: 400px;
}

.action-item.detailed {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 12px;
  margin-bottom: 8px;
  background: rgba(0,0,0,0.02);
  border-radius: 8px;
  border-left: 3px solid var(--primary);
}

.action-content {
  flex: 1;
}

.action-item.detailed .action-text {
  font-size: 13px;
  color: #1a1a1a;
  margin-bottom: 4px;
  line-height: 1.4;
}

.action-item.detailed .action-time {
  font-size: 11px;
  color: var(--muted);
}

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 40px 20px;
  color: var(--muted);
  text-align: center;
}

.empty-state .el-icon {
  font-size: 32px;
  margin-bottom: 8px;
  opacity: 0.5;
}

.panel-footer {
  padding: 16px 20px;
  border-top: 1px solid rgba(0,0,0,0.06);
  text-align: center;
}

.clear-btn {
  width: 100%;
}

.view-all-actions {
  padding: 12px 0 0 0;
  border-top: 1px solid rgba(0,0,0,0.04);
  text-align: center;
}

.view-all-btn {
  width: 100%;
}

/* 底部状态栏 */
.footer-status {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 24px;
  background: var(--card-bg);
  border-top: 1px solid rgba(0,0,0,0.06);
  font-size: 12px;
  color: var(--muted);
}

.status-left,
.status-right {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* 右键菜单 */
.context-menu {
  position: absolute;
  background: #fff;
  border-radius: 10px;
  box-shadow: 0 12px 28px rgba(8,16,32,0.12);
  padding: 6px 0;
  z-index: 9999;
  min-width: 160px;
  user-select: none;
  border: 1px solid rgba(10,20,40,0.06);
  animation: fadeInScale 0.14s ease;
}
.context-menu div {
  padding: 10px 14px;
  cursor: pointer;
  display: flex;
  gap: 8px;
  align-items: center;
  color: #222;
  font-size: 14px;
}
.context-menu div:hover {
  background: linear-gradient(90deg, rgba(0,116,217,0.08), rgba(0,188,212,0.04));
  color: var(--primary);
}

/* 模态框样式保持不变 */
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 10000;
}

.property-modal {
  background: white;
  border-radius: 8px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
  width: 400px;
  max-width: 90vw;
  animation: modalSlideIn 0.2s ease-out;
}

.modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 20px;
  border-bottom: 1px solid #e0e0e0;
}

.modal-header h3 {
  margin: 0;
  color: #333;
}

.close-btn {
  background: none;
  border: none;
  font-size: 24px;
  cursor: pointer;
  color: #666;
}

.close-btn:hover {
  color: #333;
}

.modal-content {
  padding: 20px;
}

.form-group {
  margin-bottom: 16px;
}

.form-group label {
  display: block;
  margin-bottom: 6px;
  font-weight: 500;
  color: #555;
}

.form-group input {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 14px;
  box-sizing: border-box;
}

.readonly-input {
  background-color: #f5f5f5;
  color: #666;
  cursor: not-allowed;
}

.position-info {
  padding: 8px 12px;
  background-color: #f9f9f9;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
  font-size: 14px;
  color: #666;
}

.modal-footer {
  display: flex;
  justify-content: flex-end;
  gap: 12px;
  padding: 16px 20px;
  border-top: 1px solid #e0e0e0;
}

.btn {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

.btn-primary {
  background-color: #0074d9;
  color: white;
}

.btn-primary:hover {
  background-color: #0056b3;
}

.btn-secondary {
  background-color: #6c757d;
  color: white;
}

.btn-secondary:hover {
  background-color: #545b62;
}

@keyframes modalSlideIn {
  from {
    opacity: 0;
    transform: translateY(-20px) scale(0.95);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@keyframes fadeInScale {
  from { opacity: 0; transform: scale(0.98); }
  to { opacity: 1; transform: scale(1); }
}

/* 响应式设计 */
@media (max-width: 1200px) {
  .main-content {
    flex-direction: column;
  }
  
  .left-sidebar,
  .right-sidebar {
    width: 100%;
    flex-direction: row;
    overflow-x: auto;
  }
  
  .mode-card,
  .stats-card,
  .quick-actions-card,
  .selection-card,
  .status-card,
  .recent-actions-card {
    min-width: 250px;
  }
}


@media (max-width: 900px) {
  .topology-overlay { top: 20px; left: 12px; }
}

@media (max-width: 768px) {
  .header-section {
    flex-direction: column;
    gap: 12px;
    text-align: center;
  }
  
  .top-bar {
    flex-direction: column;
    gap: 12px;
  }
  
  .left-sidebar,
  .right-sidebar {
    flex-direction: column;
  }
}
</style>